@triflux/core 10.0.0-alpha.1 → 10.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.
Files changed (38) hide show
  1. package/hooks/hook-adaptive-collector.mjs +86 -0
  2. package/hooks/hook-manager.mjs +15 -2
  3. package/hooks/hook-registry.json +37 -4
  4. package/hooks/keyword-rules.json +2 -1
  5. package/hooks/mcp-config-watcher.mjs +2 -7
  6. package/hooks/safety-guard.mjs +37 -0
  7. package/hub/account-broker.mjs +251 -0
  8. package/hub/adaptive-diagnostic.mjs +323 -0
  9. package/hub/adaptive-inject.mjs +186 -0
  10. package/hub/adaptive-memory.mjs +163 -0
  11. package/hub/adaptive.mjs +143 -0
  12. package/hub/cli-adapter-base.mjs +89 -1
  13. package/hub/codex-adapter.mjs +12 -3
  14. package/hub/codex-compat.mjs +11 -78
  15. package/hub/codex-preflight.mjs +20 -1
  16. package/hub/gemini-adapter.mjs +1 -0
  17. package/hub/index.mjs +34 -0
  18. package/hub/lib/cache-guard.mjs +114 -0
  19. package/hub/lib/known-errors.json +72 -0
  20. package/hub/lib/memory-store.mjs +748 -0
  21. package/hub/lib/ssh-command.mjs +150 -0
  22. package/hub/lib/uuidv7.mjs +44 -0
  23. package/hub/memory-doctor.mjs +480 -0
  24. package/hub/middleware/request-logger.mjs +80 -0
  25. package/hub/router.mjs +1 -1
  26. package/hub/team-bridge.mjs +21 -19
  27. package/hud/constants.mjs +7 -0
  28. package/hud/context-monitor.mjs +403 -0
  29. package/hud/hud-qos-status.mjs +8 -4
  30. package/hud/providers/claude.mjs +5 -0
  31. package/hud/renderers.mjs +32 -14
  32. package/hud/utils.mjs +26 -0
  33. package/package.json +3 -2
  34. package/scripts/lib/claudemd-scanner.mjs +218 -0
  35. package/scripts/lib/handoff.mjs +171 -0
  36. package/scripts/lib/mcp-guard-engine.mjs +20 -6
  37. package/scripts/lib/skill-template.mjs +269 -0
  38. package/scripts/lib/claudemd-manager.mjs +0 -325
@@ -0,0 +1,323 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { normalizeError } from './reflexion.mjs';
6
+
7
+ const DEFAULT_KNOWN_ERRORS_PATH = resolve(
8
+ dirname(fileURLToPath(import.meta.url)),
9
+ 'lib/known-errors.json',
10
+ );
11
+ const DEFAULT_CONFIDENCE = 0.5;
12
+ const ADAPTIVE_CONFIDENCE_STEP = 0.1;
13
+ const MAX_ADAPTIVE_CONFIDENCE = 0.95;
14
+
15
+ function clone(value) {
16
+ return value == null ? value : JSON.parse(JSON.stringify(value));
17
+ }
18
+
19
+ function pickString(...values) {
20
+ for (const value of values) {
21
+ if (typeof value === 'string' && value.trim()) return value.trim();
22
+ }
23
+ return '';
24
+ }
25
+
26
+ function pickObject(...values) {
27
+ return values.find(
28
+ (value) => value && typeof value === 'object' && !Array.isArray(value),
29
+ ) || {};
30
+ }
31
+
32
+ function clampConfidence(value, fallback = DEFAULT_CONFIDENCE) {
33
+ const numeric = Number(value);
34
+ if (!Number.isFinite(numeric)) return fallback;
35
+ return Math.max(0, Math.min(1, numeric));
36
+ }
37
+
38
+ function normalizeLookup(text) {
39
+ return String(text || '').trim().toLowerCase();
40
+ }
41
+
42
+ function readPathValue(source, path) {
43
+ if (!source || !path) return undefined;
44
+ return String(path)
45
+ .split('.')
46
+ .filter(Boolean)
47
+ .reduce((current, key) => current?.[key], source);
48
+ }
49
+
50
+ function renderTemplate(template, observation, signature) {
51
+ if (!template) return '';
52
+ const context = pickObject(observation.context);
53
+ const dna = pickObject(observation.dna);
54
+ const dnaValue = signature.dna_factor
55
+ ? readPathValue(dna, signature.dna_factor) ??
56
+ readPathValue(context, signature.dna_factor) ??
57
+ readPathValue(observation, signature.dna_factor)
58
+ : undefined;
59
+ const variables = {
60
+ ...context,
61
+ ...pickObject(observation.variables),
62
+ host: pickString(observation.host, context.host),
63
+ value: dnaValue,
64
+ };
65
+ return String(template).replace(/\{([^}]+)\}/gu, (match, key) => {
66
+ const value = variables[key];
67
+ return value == null || value === '' ? match : String(value);
68
+ });
69
+ }
70
+
71
+ function buildKnownContextText(observation) {
72
+ return normalizeLookup(
73
+ [
74
+ pickString(observation.contextLabel, observation.context),
75
+ pickString(observation.phase),
76
+ pickString(observation.step),
77
+ ]
78
+ .filter(Boolean)
79
+ .join(' '),
80
+ );
81
+ }
82
+
83
+ function buildErrorText(observation = {}) {
84
+ return [
85
+ pickString(observation.error),
86
+ pickString(observation.stderr),
87
+ pickString(observation.tool_output, observation.output),
88
+ pickString(observation.message),
89
+ ]
90
+ .filter(Boolean)
91
+ .join('\n')
92
+ .trim();
93
+ }
94
+
95
+ function normalizeObservation(observation = {}) {
96
+ const errorText = buildErrorText(observation);
97
+ const projectSlug = pickString(
98
+ observation.project_slug,
99
+ observation.projectSlug,
100
+ observation.context?.project_slug,
101
+ observation.context?.projectSlug,
102
+ );
103
+ return {
104
+ ...clone(observation),
105
+ errorText,
106
+ errorPattern: normalizeError(errorText),
107
+ projectSlug,
108
+ tool: pickString(observation.tool, observation.tool_name),
109
+ contextText: buildKnownContextText(observation),
110
+ };
111
+ }
112
+
113
+ function compileSignatures(raw = {}) {
114
+ return Object.entries(raw.signatures || {}).map(([id, signature]) => ({
115
+ id,
116
+ ...clone(signature),
117
+ matcher: new RegExp(String(signature.pattern || ''), 'iu'),
118
+ }));
119
+ }
120
+
121
+ export function loadKnownErrors(filePath = DEFAULT_KNOWN_ERRORS_PATH) {
122
+ try {
123
+ const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
124
+ return {
125
+ path: filePath,
126
+ version: parsed.version ?? 1,
127
+ signatures: compileSignatures(parsed),
128
+ };
129
+ } catch {
130
+ return { path: filePath, version: 0, signatures: [] };
131
+ }
132
+ }
133
+
134
+ function scoreKnownMatch(signature, observation) {
135
+ if (!observation.errorText || !signature.matcher.test(observation.errorText)) {
136
+ return null;
137
+ }
138
+ if (
139
+ signature.tool &&
140
+ observation.tool &&
141
+ normalizeLookup(signature.tool) !== normalizeLookup(observation.tool)
142
+ ) {
143
+ return null;
144
+ }
145
+ if (
146
+ signature.context &&
147
+ observation.contextText &&
148
+ !observation.contextText.includes(normalizeLookup(signature.context))
149
+ ) {
150
+ return null;
151
+ }
152
+ return clampConfidence(
153
+ Number(signature.confidence_base ?? DEFAULT_CONFIDENCE) +
154
+ (signature.tool && observation.tool ? 0.02 : 0) +
155
+ (signature.context && observation.contextText ? 0.02 : 0) +
156
+ (signature.dna_factor ? 0.01 : 0),
157
+ );
158
+ }
159
+
160
+ function severityFromConfidence(confidence, fallback = 'medium') {
161
+ if (confidence >= 0.9) return 'critical';
162
+ if (confidence >= 0.75) return 'high';
163
+ if (confidence >= 0.55) return 'medium';
164
+ return fallback;
165
+ }
166
+
167
+ export function matchKnownError(catalog, observationInput = {}) {
168
+ const observation = normalizeObservation(observationInput);
169
+ const matched = (catalog?.signatures || [])
170
+ .map((signature) => {
171
+ const confidence = scoreKnownMatch(signature, observation);
172
+ return confidence == null ? null : { signature, confidence };
173
+ })
174
+ .filter(Boolean)
175
+ .sort((left, right) => right.confidence - left.confidence)[0];
176
+
177
+ if (!matched) return null;
178
+ const { signature, confidence } = matched;
179
+ return {
180
+ matched: true,
181
+ source: 'known',
182
+ signature_id: signature.id,
183
+ project_slug: observation.projectSlug,
184
+ error_pattern: observation.errorPattern,
185
+ error_message: observation.errorText,
186
+ confidence,
187
+ severity: signature.severity || severityFromConfidence(confidence),
188
+ tool: signature.tool || observation.tool,
189
+ context: signature.context || observation.contextText || null,
190
+ root_cause: signature.root_cause || 'known failure pattern',
191
+ rule: renderTemplate(signature.rule_template, observation, signature),
192
+ fix: signature.fix || null,
193
+ dna_factor: signature.dna_factor || null,
194
+ };
195
+ }
196
+
197
+ function resolveRuleStore(options = {}) {
198
+ return options.store || options.adaptiveMemory || null;
199
+ }
200
+
201
+ function ensureAdaptiveRule(store, observation) {
202
+ if (!store?.findAdaptiveRule || !store?.addAdaptiveRule || !observation.projectSlug) {
203
+ return null;
204
+ }
205
+ const identity = {
206
+ project_slug: observation.projectSlug,
207
+ pattern: observation.errorPattern,
208
+ };
209
+ const current = store.findAdaptiveRule(identity.project_slug, identity.pattern);
210
+ if (!current) {
211
+ return store.addAdaptiveRule(identity);
212
+ }
213
+ if (!store.updateRuleConfidence) return current;
214
+ return store.updateRuleConfidence(
215
+ identity.project_slug,
216
+ identity.pattern,
217
+ Math.min(MAX_ADAPTIVE_CONFIDENCE, current.confidence + ADAPTIVE_CONFIDENCE_STEP),
218
+ { hit_count_increment: 1 },
219
+ );
220
+ }
221
+
222
+ function buildAdaptiveDiagnosis(rule, observation) {
223
+ if (!rule) {
224
+ return {
225
+ matched: false,
226
+ source: 'novel',
227
+ project_slug: observation.projectSlug,
228
+ error_pattern: observation.errorPattern,
229
+ error_message: observation.errorText,
230
+ confidence: DEFAULT_CONFIDENCE,
231
+ severity: 'low',
232
+ root_cause: '새로운 실패 패턴으로 분류됨',
233
+ rule: '',
234
+ fix: null,
235
+ };
236
+ }
237
+ const matched = Number(rule.hit_count || 0) > 1 || Number(rule.confidence || 0) > DEFAULT_CONFIDENCE;
238
+ const confidence = clampConfidence(rule.confidence, DEFAULT_CONFIDENCE);
239
+ return {
240
+ matched,
241
+ source: matched ? 'adaptive' : 'novel',
242
+ project_slug: rule.project_slug,
243
+ error_pattern: rule.pattern,
244
+ error_message: observation.errorText,
245
+ confidence,
246
+ severity: severityFromConfidence(confidence, matched ? 'medium' : 'low'),
247
+ root_cause: matched
248
+ ? '반복 관측된 adaptive rule과 일치'
249
+ : 'adaptive memory에 첫 관측으로 저장됨',
250
+ rule: matched
251
+ ? `프로젝트 ${rule.project_slug}에서 동일 패턴이 ${rule.hit_count}회 관측되었습니다.`
252
+ : `프로젝트 ${rule.project_slug}의 adaptive memory에 패턴을 기록했습니다.`,
253
+ fix: matched ? '최근 성공한 수정/회피 전략을 재적용하세요.' : '추가 관측 후 adaptive rule을 승격하세요.',
254
+ adaptive_rule: clone(rule),
255
+ };
256
+ }
257
+
258
+ function createHealthyState(catalog) {
259
+ return {
260
+ state: 'healthy',
261
+ known_errors_count: catalog.signatures.length,
262
+ last_error: null,
263
+ };
264
+ }
265
+
266
+ function createDegradedState(error) {
267
+ return {
268
+ state: 'degraded',
269
+ known_errors_count: 0,
270
+ last_error: {
271
+ name: error?.name || 'Error',
272
+ message: error?.message || 'unknown adaptive diagnostic error',
273
+ },
274
+ };
275
+ }
276
+
277
+ export function createDiagnosticPipeline(options = {}) {
278
+ const store = resolveRuleStore(options);
279
+ let catalog = options.knownErrors ? { signatures: compileSignatures({ signatures: options.knownErrors }) } : null;
280
+ let health = catalog ? createHealthyState(catalog) : null;
281
+
282
+ function ensureCatalog() {
283
+ if (catalog) return catalog;
284
+ try {
285
+ catalog = loadKnownErrors(options.knownErrorsPath || DEFAULT_KNOWN_ERRORS_PATH);
286
+ health = createHealthyState(catalog);
287
+ } catch (error) {
288
+ catalog = { signatures: [] };
289
+ health = createDegradedState(error);
290
+ }
291
+ return catalog;
292
+ }
293
+
294
+ function diagnoseFailure(observationInput = {}) {
295
+ const observation = normalizeObservation(observationInput);
296
+ const nextCatalog = ensureCatalog();
297
+ const known = matchKnownError(nextCatalog, observation);
298
+ if (known) return known;
299
+ const adaptiveRule = observation.errorPattern
300
+ ? ensureAdaptiveRule(store, observation)
301
+ : null;
302
+ return buildAdaptiveDiagnosis(adaptiveRule, observation);
303
+ }
304
+
305
+ function getHealth() {
306
+ return clone(health || ensureCatalog() && health);
307
+ }
308
+
309
+ function listKnownErrors() {
310
+ return (ensureCatalog().signatures || []).map(({ matcher, ...signature }) => clone(signature));
311
+ }
312
+
313
+ return Object.freeze({
314
+ diagnose: diagnoseFailure,
315
+ diagnoseFailure,
316
+ run: diagnoseFailure,
317
+ getHealth,
318
+ listKnownErrors,
319
+ });
320
+ }
321
+
322
+ export { DEFAULT_KNOWN_ERRORS_PATH };
323
+ export default createDiagnosticPipeline;
@@ -0,0 +1,186 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ const SECTION_HEADING = '## Adaptive Rules (triflux auto-generated)';
5
+ const DEFAULT_MAX_RULES = 10;
6
+ const SECTION_RE = /^## Adaptive Rules \(triflux auto-generated\)$/mu;
7
+ const BLOCK_RE = /<!-- tfx-adaptive:start rule_id="([^"]+)" confidence=([0-9.]+) occurrences=(\d+) first_seen=([0-9-]+) last_seen=([0-9-]+) -->\r?\n([^\r\n]*)\r?\n<!-- tfx-adaptive:end -->/gu;
8
+
9
+ function cloneRule(rule) {
10
+ return Object.freeze({ ...rule });
11
+ }
12
+
13
+ function clampConfidence(value) {
14
+ const numeric = Number(value);
15
+ if (!Number.isFinite(numeric)) return 0;
16
+ return Math.min(1, Math.max(0, numeric));
17
+ }
18
+
19
+ function normalizeOccurrences(value) {
20
+ const numeric = Number(value);
21
+ if (!Number.isFinite(numeric)) return 1;
22
+ return Math.max(1, Math.trunc(numeric));
23
+ }
24
+
25
+ function formatConfidence(value) {
26
+ return Number(clampConfidence(value).toFixed(6)).toString();
27
+ }
28
+
29
+ function normalizeDate(value, fallback) {
30
+ const text = String(value ?? fallback ?? '').trim();
31
+ return text || fallback;
32
+ }
33
+
34
+ function normalizeRuleInput(rule, fallback = {}) {
35
+ if (!rule || typeof rule !== 'object') return null;
36
+
37
+ const id = String(rule.id ?? rule.rule_id ?? fallback.id ?? '').trim();
38
+ const text = String(rule.rule ?? rule.text ?? fallback.rule ?? '').trim();
39
+ const firstSeen = normalizeDate(
40
+ rule.firstSeen ?? rule.first_seen,
41
+ fallback.firstSeen ?? fallback.lastSeen ?? '1970-01-01',
42
+ );
43
+ const lastSeen = normalizeDate(rule.lastSeen ?? rule.last_seen, fallback.lastSeen ?? firstSeen);
44
+ if (!id || !text || /["\r\n]/u.test(id) || /[\r\n]/u.test(text)) {
45
+ return null;
46
+ }
47
+
48
+ return cloneRule({
49
+ id,
50
+ rule: text,
51
+ confidence: clampConfidence(rule.confidence ?? fallback.confidence),
52
+ occurrences: normalizeOccurrences(rule.occurrences ?? fallback.occurrences),
53
+ firstSeen,
54
+ lastSeen,
55
+ });
56
+ }
57
+
58
+ function trimLeadingBlankLines(text) {
59
+ return String(text ?? '').replace(/^(?:[ \t]*\r?\n)+/u, '');
60
+ }
61
+
62
+ function trimTrailingBlankLines(text) {
63
+ return String(text ?? '').replace(/(?:\r?\n[ \t]*)+$/u, '');
64
+ }
65
+
66
+ function parseInjectedRules(sectionBody = '') {
67
+ const matches = Array.from(String(sectionBody).matchAll(BLOCK_RE));
68
+ return matches.map(([, id, confidence, occurrences, firstSeen, lastSeen, text]) => cloneRule({
69
+ id,
70
+ rule: text,
71
+ confidence: clampConfidence(confidence),
72
+ occurrences: normalizeOccurrences(occurrences),
73
+ firstSeen,
74
+ lastSeen,
75
+ }));
76
+ }
77
+
78
+ function readDocument(claudeMdPath) {
79
+ const raw = existsSync(claudeMdPath) ? readFileSync(claudeMdPath, 'utf8') : '';
80
+ const sectionStart = raw.search(SECTION_RE);
81
+ if (sectionStart === -1) {
82
+ return { before: raw, after: '', rules: [] };
83
+ }
84
+
85
+ const headingEnd = raw.indexOf('\n', sectionStart);
86
+ const bodyStart = headingEnd === -1 ? raw.length : headingEnd + 1;
87
+ const rest = raw.slice(bodyStart);
88
+ const nextHeadingOffset = rest.search(/^#{1,6}\s/mu);
89
+ const sectionEnd = nextHeadingOffset === -1 ? raw.length : bodyStart + nextHeadingOffset;
90
+ const body = raw.slice(bodyStart, sectionEnd);
91
+
92
+ return {
93
+ before: raw.slice(0, sectionStart),
94
+ after: raw.slice(sectionEnd),
95
+ rules: parseInjectedRules(body),
96
+ };
97
+ }
98
+
99
+ function serializeRule(rule) {
100
+ return [
101
+ `<!-- tfx-adaptive:start rule_id="${rule.id}" confidence=${formatConfidence(rule.confidence)} occurrences=${rule.occurrences} first_seen=${rule.firstSeen} last_seen=${rule.lastSeen} -->`,
102
+ rule.rule,
103
+ '<!-- tfx-adaptive:end -->',
104
+ ].join('\n');
105
+ }
106
+
107
+ function serializeDocument(before, rules, after) {
108
+ const section = rules.length > 0
109
+ ? `${SECTION_HEADING}\n\n${rules.map(serializeRule).join('\n\n')}`
110
+ : '';
111
+ const parts = [trimTrailingBlankLines(before), section, trimLeadingBlankLines(after)].filter(Boolean);
112
+ return parts.length > 0 ? `${parts.join('\n\n')}\n` : '';
113
+ }
114
+
115
+ function enforceMaxRules(rules, maxRules) {
116
+ if (rules.length <= maxRules) return rules.map(cloneRule);
117
+ const ranked = rules
118
+ .map((rule, index) => ({ ...rule, index }))
119
+ .sort((left, right) => (
120
+ left.confidence - right.confidence
121
+ || left.occurrences - right.occurrences
122
+ || left.lastSeen.localeCompare(right.lastSeen)
123
+ || left.index - right.index
124
+ || left.id.localeCompare(right.id)
125
+ ));
126
+ const removedIds = new Set(ranked.slice(0, rules.length - maxRules).map((rule) => rule.id));
127
+ return rules.filter((rule) => !removedIds.has(rule.id)).map(cloneRule);
128
+ }
129
+
130
+ export function createAdaptiveInjector(opts = {}) {
131
+ const claudeMdPath = resolve(opts.claudeMdPath ?? join(process.cwd(), 'CLAUDE.md'));
132
+ const maxRules = Number.isInteger(opts.maxRules) && opts.maxRules > 0 ? opts.maxRules : DEFAULT_MAX_RULES;
133
+
134
+ function listInjected() {
135
+ return readDocument(claudeMdPath).rules.map(cloneRule);
136
+ }
137
+
138
+ function inject(rule) {
139
+ const document = readDocument(claudeMdPath);
140
+ const targetId = String(rule?.id ?? rule?.rule_id ?? '').trim();
141
+ const existing = document.rules.find((item) => item.id === targetId);
142
+ const normalized = normalizeRuleInput(rule, existing ?? {});
143
+ if (!normalized) return false;
144
+
145
+ const nextRules = existing
146
+ ? document.rules.map((item) => (item.id === normalized.id
147
+ ? cloneRule({
148
+ ...item,
149
+ confidence: normalized.confidence,
150
+ occurrences: normalized.occurrences,
151
+ lastSeen: normalized.lastSeen,
152
+ })
153
+ : cloneRule(item)))
154
+ : [...document.rules.map(cloneRule), normalized];
155
+ const limitedRules = enforceMaxRules(nextRules, maxRules);
156
+ writeFileSync(claudeMdPath, serializeDocument(document.before, limitedRules, document.after), 'utf8');
157
+ return limitedRules.some((item) => item.id === normalized.id);
158
+ }
159
+
160
+ function remove(ruleId) {
161
+ const targetId = String(ruleId ?? '').trim();
162
+ if (!targetId || !existsSync(claudeMdPath)) return false;
163
+ const document = readDocument(claudeMdPath);
164
+ if (!document.rules.some((rule) => rule.id === targetId)) return false;
165
+ const nextRules = document.rules.filter((rule) => rule.id !== targetId).map(cloneRule);
166
+ writeFileSync(claudeMdPath, serializeDocument(document.before, nextRules, document.after), 'utf8');
167
+ return true;
168
+ }
169
+
170
+ function cleanup(activeRuleIds = []) {
171
+ const activeIds = new Set(Array.isArray(activeRuleIds) ? activeRuleIds : Array.from(activeRuleIds));
172
+ return listInjected().reduce(
173
+ (count, rule) => count + (activeIds.has(rule.id) ? 0 : Number(remove(rule.id))),
174
+ 0,
175
+ );
176
+ }
177
+
178
+ return Object.freeze({
179
+ inject,
180
+ remove,
181
+ listInjected,
182
+ cleanup,
183
+ });
184
+ }
185
+
186
+ export default createAdaptiveInjector;
@@ -0,0 +1,163 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ const SESSION_FILE = 'adaptive-session.json';
6
+ const DEFAULT_CONFIDENCE = 0.5;
7
+ const TIER2_DECAY_STEP = 0.2;
8
+ const TIER2_DECAY_INTERVAL = 5;
9
+ const TIER2_REMOVE_THRESHOLD = 0.3;
10
+ const TIER3_WARN_THRESHOLD = 10;
11
+ const TIER3_REMOVE_THRESHOLD = 20;
12
+
13
+ const clone = (v) => (v == null ? v : JSON.parse(JSON.stringify(v)));
14
+ const clamp01 = (v) => { const n = Number(v); return Number.isFinite(n) ? Number(Math.max(0, Math.min(1, n)).toFixed(4)) : DEFAULT_CONFIDENCE; };
15
+ const toDate = (v = Date.now()) => { const d = new Date(v); return Number.isNaN(d.getTime()) ? new Date().toISOString().slice(0, 10) : d.toISOString().slice(0, 10); };
16
+ const uniq = (arr) => [...new Set(arr.filter((s) => typeof s === 'string' && s.trim()))];
17
+ const slugify = (v) => String(v || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80);
18
+ const buildId = (r) => { const e = slugify(r.id); if (e) return e; const p = slugify(r.pattern); return p || `adaptive-${Date.now()}`; };
19
+ const readJson = (f, fb) => { if (!existsSync(f)) return clone(fb); try { return { ...clone(fb), ...JSON.parse(readFileSync(f, 'utf8')) }; } catch { return clone(fb); } };
20
+ const writeJson = (f, v) => writeFileSync(f, `${JSON.stringify(v, null, 2)}\n`, 'utf8');
21
+ const strip = (r) => { if (!r) return null; const { sessionIds, ...pub } = r; return clone(pub); };
22
+ const sortRules = (list) => [...list].sort((a, b) => a.tier - b.tier || b.confidence - a.confidence || a.id.localeCompare(b.id));
23
+ const upsert = (list, r) => [...list.filter((x) => x.id !== r.id), r];
24
+ const without = (list, id) => list.filter((x) => x.id !== id);
25
+
26
+ function toRule(rule, tier, sessionId, existing = null) {
27
+ return {
28
+ id: existing?.id || buildId(rule),
29
+ pattern: String(rule.pattern || existing?.pattern || ''),
30
+ rootCause: String(rule.rootCause || rule.root_cause || existing?.rootCause || ''),
31
+ rule: String(rule.rule || existing?.rule || ''),
32
+ confidence: clamp01(rule.confidence ?? existing?.confidence),
33
+ occurrences: Math.max(1, Number(existing?.occurrences || 0) + 1),
34
+ firstSeen: existing?.firstSeen || toDate(rule.timestamp),
35
+ lastSeen: toDate(rule.timestamp),
36
+ sessionsWithout: 0,
37
+ tier,
38
+ dnaFactor: rule.dnaFactor ?? rule.dna_factor ?? existing?.dnaFactor ?? null,
39
+ sessionIds: uniq([...(existing?.sessionIds || []), sessionId]),
40
+ };
41
+ }
42
+
43
+ export function createAdaptiveMemory(opts = {}) {
44
+ const projectSlug = slugify(opts.projectSlug);
45
+ if (!projectSlug) throw new Error('projectSlug is required');
46
+
47
+ const sessionDir = opts.sessionDir || join(process.cwd(), '.omc', 'state');
48
+ const globalDir = opts.globalDir || join(homedir(), '.triflux', 'adaptive');
49
+ const sessionFile = join(sessionDir, SESSION_FILE);
50
+ const projectFile = join(globalDir, `${projectSlug}.json`);
51
+
52
+ mkdirSync(sessionDir, { recursive: true });
53
+ mkdirSync(globalDir, { recursive: true });
54
+
55
+ let ss = readJson(sessionFile, { projectSlug, sessionId: null, rules: [] });
56
+ let ps = readJson(projectFile, { projectSlug, history: {}, tier2: [], tier3: [] });
57
+
58
+ function saveSession() {
59
+ if (ss.rules.length === 0) { rmSync(sessionFile, { force: true }); return; }
60
+ writeJson(sessionFile, ss);
61
+ }
62
+
63
+ function saveProject() {
64
+ const has = Object.keys(ps.history).length > 0 || ps.tier2.length > 0 || ps.tier3.length > 0;
65
+ if (!has) { rmSync(projectFile, { force: true }); return; }
66
+ writeJson(projectFile, ps);
67
+ }
68
+
69
+ const saveAll = () => { saveSession(); saveProject(); };
70
+
71
+ function getSessionId(id) {
72
+ const next = String(id || ss.sessionId || 'session-current');
73
+ ss = { ...ss, sessionId: next };
74
+ return next;
75
+ }
76
+
77
+ const getDurable = (id) => ps.tier3.find((r) => r.id === id) || ps.tier2.find((r) => r.id === id) || null;
78
+
79
+ function promote(ruleId) {
80
+ const t2 = ps.tier2.find((r) => r.id === ruleId);
81
+ if (t2 && t2.occurrences >= 3 && t2.confidence >= 0.8) {
82
+ const next = { ...t2, tier: 3, sessionsWithout: 0 };
83
+ ps = { ...ps, tier2: without(ps.tier2, ruleId), tier3: upsert(ps.tier3, next) };
84
+ saveProject();
85
+ return { rule: strip(next), promoted: true, fromTier: 2, toTier: 3 };
86
+ }
87
+ const cand = ps.history[ruleId];
88
+ if (cand && cand.occurrences >= 2 && cand.sessionIds.length >= 2) {
89
+ const next = { ...cand, tier: 2, sessionsWithout: 0 };
90
+ const { [ruleId]: _, ...history } = ps.history;
91
+ ps = { ...ps, history, tier2: upsert(ps.tier2, next) };
92
+ ss = { ...ss, rules: without(ss.rules, ruleId) };
93
+ saveAll();
94
+ return { rule: strip(next), promoted: true, fromTier: 1, toTier: 2 };
95
+ }
96
+ const cur = getDurable(ruleId) || ps.history[ruleId] || null;
97
+ return { rule: strip(cur), promoted: false, fromTier: cur?.tier ?? null, toTier: cur?.tier ?? null };
98
+ }
99
+
100
+ function record(rule = {}) {
101
+ if (!rule.pattern) return { rule: null, promoted: false, fromTier: null, toTier: null };
102
+ const sessionId = getSessionId(rule.sessionId);
103
+ const ruleId = buildId(rule);
104
+ const durable = getDurable(ruleId);
105
+ if (durable) {
106
+ const next = toRule(rule, durable.tier, sessionId, durable);
107
+ ps = durable.tier === 3 ? { ...ps, tier3: upsert(ps.tier3, next) } : { ...ps, tier2: upsert(ps.tier2, next) };
108
+ saveProject();
109
+ const res = promote(ruleId);
110
+ return res.promoted ? res : { rule: strip(next), promoted: false, fromTier: durable.tier, toTier: durable.tier };
111
+ }
112
+ const cand = toRule(rule, 1, sessionId, ps.history[ruleId]);
113
+ ps = { ...ps, history: { ...ps.history, [ruleId]: cand } };
114
+ ss = { ...ss, rules: upsert(ss.rules, { ...cand, tier: 1 }) };
115
+ saveAll();
116
+ const res = promote(ruleId);
117
+ return res.promoted ? res : { rule: strip(cand), promoted: false, fromTier: 1, toTier: 1 };
118
+ }
119
+
120
+ function decay(sessionId) {
121
+ const nextId = String(sessionId || `session-${Date.now()}`);
122
+ if (ss.sessionId === nextId) return { sessionId: nextId, tier1Cleared: 0, updated: [], warned: [], removed: [] };
123
+ const warned = [], removed = [], updated = [];
124
+ const tier1Cleared = ss.rules.length;
125
+ const tier2 = ps.tier2.flatMap((r) => {
126
+ const sw = Number(r.sessionsWithout || 0) + 1;
127
+ const conf = sw % TIER2_DECAY_INTERVAL === 0 ? clamp01(r.confidence - TIER2_DECAY_STEP) : r.confidence;
128
+ if (conf < TIER2_REMOVE_THRESHOLD) { removed.push(r.id); return []; }
129
+ updated.push(r.id);
130
+ return [{ ...r, sessionsWithout: sw, confidence: conf }];
131
+ });
132
+ const tier3 = ps.tier3.flatMap((r) => {
133
+ const sw = Number(r.sessionsWithout || 0) + 1;
134
+ if (sw >= TIER3_REMOVE_THRESHOLD) { removed.push(r.id); return []; }
135
+ if (sw === TIER3_WARN_THRESHOLD) warned.push(r.id);
136
+ updated.push(r.id);
137
+ return [{ ...r, sessionsWithout: sw }];
138
+ });
139
+ ss = { ...ss, sessionId: nextId, rules: [] };
140
+ ps = { ...ps, tier2, tier3 };
141
+ saveAll();
142
+ return { sessionId: nextId, tier1Cleared, updated, warned, removed };
143
+ }
144
+
145
+ function getRule(id) { return strip(ss.rules.find((r) => r.id === id) || getDurable(id)); }
146
+ function getTier(tier) {
147
+ if (tier === 1) return sortRules(ss.rules).map(strip);
148
+ if (tier === 2) return sortRules(ps.tier2).map(strip);
149
+ if (tier === 3) return sortRules(ps.tier3).map(strip);
150
+ return [];
151
+ }
152
+ function getAllRules() { return sortRules([...ss.rules, ...ps.tier2, ...ps.tier3]).map(strip); }
153
+ function reset(target = 'all') {
154
+ const rs = target === 'all' || target === 1 || target === 'session';
155
+ const rp = target === 'all' || target === 2 || target === 3 || target === 'project';
156
+ if (rs) ss = { ...ss, rules: [] };
157
+ if (rp) ps = { ...ps, history: {}, tier2: [], tier3: [] };
158
+ saveAll();
159
+ return { sessionCleared: rs, projectCleared: rp };
160
+ }
161
+
162
+ return { record, promote, decay, getRule, getAllRules, getTier, reset };
163
+ }