@openanonymity/nanomem 0.1.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 (66) hide show
  1. package/README.md +194 -0
  2. package/package.json +85 -0
  3. package/src/backends/BaseStorage.js +177 -0
  4. package/src/backends/filesystem.js +177 -0
  5. package/src/backends/indexeddb.js +208 -0
  6. package/src/backends/ram.js +113 -0
  7. package/src/backends/schema.js +42 -0
  8. package/src/bullets/bulletIndex.js +125 -0
  9. package/src/bullets/compaction.js +109 -0
  10. package/src/bullets/index.js +16 -0
  11. package/src/bullets/normalize.js +241 -0
  12. package/src/bullets/parser.js +199 -0
  13. package/src/bullets/scoring.js +53 -0
  14. package/src/cli/auth.js +323 -0
  15. package/src/cli/commands.js +411 -0
  16. package/src/cli/config.js +120 -0
  17. package/src/cli/diff.js +68 -0
  18. package/src/cli/help.js +84 -0
  19. package/src/cli/output.js +269 -0
  20. package/src/cli/spinner.js +54 -0
  21. package/src/cli.js +178 -0
  22. package/src/engine/compactor.js +247 -0
  23. package/src/engine/executors.js +152 -0
  24. package/src/engine/ingester.js +229 -0
  25. package/src/engine/retriever.js +414 -0
  26. package/src/engine/toolLoop.js +176 -0
  27. package/src/imports/chatgpt.js +160 -0
  28. package/src/imports/index.js +14 -0
  29. package/src/imports/markdown.js +104 -0
  30. package/src/imports/oaFastchat.js +124 -0
  31. package/src/index.js +199 -0
  32. package/src/llm/anthropic.js +264 -0
  33. package/src/llm/openai.js +179 -0
  34. package/src/prompt_sets/conversation/ingestion.js +51 -0
  35. package/src/prompt_sets/document/ingestion.js +43 -0
  36. package/src/prompt_sets/index.js +31 -0
  37. package/src/types.js +382 -0
  38. package/src/utils/portability.js +174 -0
  39. package/types/backends/BaseStorage.d.ts +42 -0
  40. package/types/backends/filesystem.d.ts +11 -0
  41. package/types/backends/indexeddb.d.ts +12 -0
  42. package/types/backends/ram.d.ts +8 -0
  43. package/types/backends/schema.d.ts +14 -0
  44. package/types/bullets/bulletIndex.d.ts +47 -0
  45. package/types/bullets/compaction.d.ts +10 -0
  46. package/types/bullets/index.d.ts +36 -0
  47. package/types/bullets/normalize.d.ts +95 -0
  48. package/types/bullets/parser.d.ts +31 -0
  49. package/types/bullets/scoring.d.ts +12 -0
  50. package/types/engine/compactor.d.ts +27 -0
  51. package/types/engine/executors.d.ts +46 -0
  52. package/types/engine/ingester.d.ts +29 -0
  53. package/types/engine/retriever.d.ts +50 -0
  54. package/types/engine/toolLoop.d.ts +9 -0
  55. package/types/imports/chatgpt.d.ts +14 -0
  56. package/types/imports/index.d.ts +3 -0
  57. package/types/imports/markdown.d.ts +31 -0
  58. package/types/imports/oaFastchat.d.ts +30 -0
  59. package/types/index.d.ts +21 -0
  60. package/types/llm/anthropic.d.ts +16 -0
  61. package/types/llm/openai.d.ts +16 -0
  62. package/types/prompt_sets/conversation/ingestion.d.ts +7 -0
  63. package/types/prompt_sets/document/ingestion.d.ts +7 -0
  64. package/types/prompt_sets/index.d.ts +11 -0
  65. package/types/types.d.ts +293 -0
  66. package/types/utils/portability.d.ts +33 -0
@@ -0,0 +1,208 @@
1
+ /**
2
+ * IndexedDBStorage — Virtual markdown filesystem backed by IndexedDB.
3
+ *
4
+ * Browser-only. Stores memory files in a separate 'oa-memory-fs' database.
5
+ */
6
+ /** @import { ExportRecord, StorageMetadata } from '../types.js' */
7
+ import { BaseStorage } from './BaseStorage.js';
8
+ import { countBullets, extractTitles } from '../bullets/index.js';
9
+ import { buildTree, createBootstrapRecords } from './schema.js';
10
+
11
+ const DB_NAME = 'oa-memory-fs';
12
+ const DB_VERSION = 1;
13
+ const STORE_NAME = 'memoryFiles';
14
+
15
+ class IndexedDBStorage extends BaseStorage {
16
+ constructor() {
17
+ super();
18
+ /** @type {IDBDatabase | null} */
19
+ this.db = null;
20
+ /** @type {Promise<IDBDatabase> | null} */
21
+ this._initPromise = null;
22
+ }
23
+
24
+ /** @returns {Promise<void>} */
25
+ async init() {
26
+ if (this.db) return;
27
+ if (this._initPromise) {
28
+ await this._initPromise;
29
+ return;
30
+ }
31
+
32
+ this._initPromise = /** @type {Promise<IDBDatabase>} */ (new Promise((resolve, reject) => {
33
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
34
+
35
+ request.onupgradeneeded = (event) => {
36
+ const db = /** @type {IDBOpenDBRequest} */ (event.target).result;
37
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
38
+ const store = db.createObjectStore(STORE_NAME, { keyPath: 'path' });
39
+ store.createIndex('parentPath', 'parentPath', { unique: false });
40
+ }
41
+ };
42
+
43
+ request.onsuccess = async (event) => {
44
+ this.db = /** @type {IDBOpenDBRequest} */ (event.target).result;
45
+ try { await this._bootstrap(); } catch (err) {
46
+ console.warn('[IndexedDBStorage] Init error:', err);
47
+ }
48
+ resolve(/** @type {IDBDatabase} */ (this.db));
49
+ };
50
+
51
+ request.onerror = () => {
52
+ this._initPromise = null;
53
+ reject(request.error);
54
+ };
55
+ }));
56
+
57
+ await this._initPromise;
58
+ }
59
+
60
+ async _bootstrap() {
61
+ const all = await this._getAll();
62
+ if (all.length > 0) return;
63
+
64
+ const seeds = createBootstrapRecords(Date.now());
65
+ return /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
66
+ const tx = /** @type {IDBDatabase} */ (this.db).transaction(STORE_NAME, 'readwrite');
67
+ const store = tx.objectStore(STORE_NAME);
68
+ for (const seed of seeds) store.put(seed);
69
+ tx.oncomplete = () => resolve();
70
+ tx.onerror = () => reject(tx.error);
71
+ }));
72
+ }
73
+
74
+ async _readRaw(path) {
75
+ await this.init();
76
+ return new Promise((resolve, reject) => {
77
+ const tx = /** @type {IDBDatabase} */ (this.db).transaction(STORE_NAME, 'readonly');
78
+ const request = tx.objectStore(STORE_NAME).get(path);
79
+ request.onsuccess = () => resolve(request.result?.content ?? null);
80
+ request.onerror = () => reject(request.error);
81
+ });
82
+ }
83
+
84
+ async _writeRaw(path, content, meta = {}) {
85
+ await this.init();
86
+ const now = Date.now();
87
+ const existing = await this._get(path);
88
+ const str = String(content || '');
89
+
90
+ const record = {
91
+ path,
92
+ content: str,
93
+ oneLiner: meta.oneLiner ?? this._generateOneLiner(str),
94
+ itemCount: meta.itemCount ?? countBullets(str),
95
+ titles: meta.titles ?? extractTitles(str),
96
+ parentPath: this._parentPath(path),
97
+ createdAt: existing?.createdAt ?? now,
98
+ updatedAt: now,
99
+ };
100
+
101
+ await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
102
+ const tx = /** @type {IDBDatabase} */ (this.db).transaction(STORE_NAME, 'readwrite');
103
+ const request = tx.objectStore(STORE_NAME).put(record);
104
+ request.onsuccess = () => resolve();
105
+ request.onerror = () => reject(request.error);
106
+ }));
107
+ }
108
+
109
+ /**
110
+ * @param {string} path
111
+ * @returns {Promise<void>}
112
+ */
113
+ async delete(path) {
114
+ if (this._isInternalPath(path)) return;
115
+ await this.init();
116
+
117
+ await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
118
+ const tx = /** @type {IDBDatabase} */ (this.db).transaction(STORE_NAME, 'readwrite');
119
+ const request = tx.objectStore(STORE_NAME).delete(path);
120
+ request.onsuccess = () => resolve();
121
+ request.onerror = () => reject(request.error);
122
+ }));
123
+ await this.rebuildTree();
124
+ }
125
+
126
+ /** @returns {Promise<void>} */
127
+ async clear() {
128
+ await this.init();
129
+ await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
130
+ const tx = /** @type {IDBDatabase} */ (this.db).transaction(STORE_NAME, 'readwrite');
131
+ const request = tx.objectStore(STORE_NAME).clear();
132
+ request.onsuccess = () => resolve();
133
+ request.onerror = () => reject(request.error);
134
+ }));
135
+ this._initPromise = null;
136
+ await this._bootstrap();
137
+ }
138
+
139
+ /**
140
+ * @param {string} path
141
+ * @returns {Promise<boolean>}
142
+ */
143
+ async exists(path) {
144
+ await this.init();
145
+ return new Promise((resolve, reject) => {
146
+ const tx = /** @type {IDBDatabase} */ (this.db).transaction(STORE_NAME, 'readonly');
147
+ const request = tx.objectStore(STORE_NAME).getKey(path);
148
+ request.onsuccess = () => resolve(request.result !== undefined);
149
+ request.onerror = () => reject(request.error);
150
+ });
151
+ }
152
+
153
+ /** @returns {Promise<void>} */
154
+ async rebuildTree() {
155
+ await this.init();
156
+ const all = await this._getAll();
157
+ const files = all
158
+ .filter((r) => !this._isInternalPath(r.path))
159
+ .sort((a, b) => a.path.localeCompare(b.path));
160
+ const indexContent = buildTree(files);
161
+ const existing = await this._get('_tree.md');
162
+ const now = Date.now();
163
+
164
+ await /** @type {Promise<void>} */ (new Promise((resolve, reject) => {
165
+ const tx = /** @type {IDBDatabase} */ (this.db).transaction(STORE_NAME, 'readwrite');
166
+ tx.objectStore(STORE_NAME).put({
167
+ path: '_tree.md',
168
+ content: indexContent,
169
+ oneLiner: 'Root index of memory filesystem',
170
+ itemCount: 0,
171
+ titles: [],
172
+ parentPath: '',
173
+ createdAt: existing?.createdAt ?? now,
174
+ updatedAt: now,
175
+ });
176
+ tx.oncomplete = () => resolve();
177
+ tx.onerror = () => reject(tx.error);
178
+ }));
179
+ }
180
+
181
+ /** @returns {Promise<ExportRecord[]>} */
182
+ async exportAll() {
183
+ await this.init();
184
+ return this._getAll();
185
+ }
186
+
187
+ // ─── Internal IndexedDB helpers ──────────────────────────────
188
+
189
+ async _get(path) {
190
+ return new Promise((resolve, reject) => {
191
+ const tx = /** @type {IDBDatabase} */ (this.db).transaction(STORE_NAME, 'readonly');
192
+ const request = tx.objectStore(STORE_NAME).get(path);
193
+ request.onsuccess = () => resolve(request.result ?? null);
194
+ request.onerror = () => reject(request.error);
195
+ });
196
+ }
197
+
198
+ async _getAll() {
199
+ return new Promise((resolve, reject) => {
200
+ const tx = /** @type {IDBDatabase} */ (this.db).transaction(STORE_NAME, 'readonly');
201
+ const request = tx.objectStore(STORE_NAME).getAll();
202
+ request.onsuccess = () => resolve(request.result || []);
203
+ request.onerror = () => reject(request.error);
204
+ });
205
+ }
206
+ }
207
+
208
+ export { IndexedDBStorage };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * InMemoryStorage — In-memory (RAM) storage backend for testing.
3
+ *
4
+ * Data is lost when the process exits.
5
+ */
6
+ /** @import { ExportRecord, StorageMetadata } from '../types.js' */
7
+ import { BaseStorage } from './BaseStorage.js';
8
+ import { countBullets, extractTitles } from '../bullets/index.js';
9
+ import { buildTree, createBootstrapRecords } from './schema.js';
10
+
11
+ class InMemoryStorage extends BaseStorage {
12
+ constructor() {
13
+ super();
14
+ this._files = new Map();
15
+ this._initialized = false;
16
+ }
17
+
18
+ async init() {
19
+ if (this._initialized) return;
20
+ this._initialized = true;
21
+
22
+ if (this._files.size === 0) {
23
+ const seeds = createBootstrapRecords(Date.now());
24
+ for (const seed of seeds) {
25
+ this._files.set(seed.path, seed);
26
+ }
27
+ }
28
+
29
+ }
30
+
31
+ async _readRaw(path) {
32
+ await this.init();
33
+ return this._files.get(path)?.content ?? null;
34
+ }
35
+
36
+ async _writeRaw(path, content, meta = {}) {
37
+ await this.init();
38
+ const now = Date.now();
39
+ const existing = this._files.get(path);
40
+ const str = String(content || '');
41
+
42
+ this._files.set(path, {
43
+ path,
44
+ content: str,
45
+ oneLiner: meta.oneLiner ?? this._generateOneLiner(str),
46
+ itemCount: meta.itemCount ?? countBullets(str),
47
+ titles: meta.titles ?? extractTitles(str),
48
+ parentPath: this._parentPath(path),
49
+ createdAt: existing?.createdAt ?? now,
50
+ updatedAt: now,
51
+ });
52
+ }
53
+
54
+ /**
55
+ * @param {string} path
56
+ * @returns {Promise<void>}
57
+ */
58
+ async delete(path) {
59
+ if (this._isInternalPath(path)) return;
60
+ await this.init();
61
+ this._files.delete(path);
62
+ await this.rebuildTree();
63
+ }
64
+
65
+ async clear() {
66
+ this._files.clear();
67
+ this._initialized = false;
68
+ await this.init();
69
+ }
70
+
71
+ /**
72
+ * @param {string} path
73
+ * @returns {Promise<boolean>}
74
+ */
75
+ async exists(path) {
76
+ await this.init();
77
+ return this._files.has(path);
78
+ }
79
+
80
+ async rebuildTree() {
81
+ await this.init();
82
+ const files = [...this._files.values()]
83
+ .filter(r => !this._isInternalPath(r.path))
84
+ .sort((a, b) => a.path.localeCompare(b.path));
85
+ const indexContent = buildTree(files);
86
+ const existing = this._files.get('_tree.md');
87
+ const now = Date.now();
88
+
89
+ this._files.set('_tree.md', {
90
+ path: '_tree.md',
91
+ content: indexContent,
92
+ oneLiner: 'Root index of memory filesystem',
93
+ itemCount: 0,
94
+ titles: [],
95
+ parentPath: '',
96
+ createdAt: existing?.createdAt ?? now,
97
+ updatedAt: now,
98
+ });
99
+ }
100
+
101
+ /** @returns {Promise<ExportRecord[]>} */
102
+ async exportAll() {
103
+ await this.init();
104
+ return [...this._files.values()];
105
+ }
106
+
107
+ async _listAllPaths() {
108
+ await this.init();
109
+ return [...this._files.keys()].filter(p => !this._isInternalPath(p));
110
+ }
111
+ }
112
+
113
+ export { InMemoryStorage };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Memory index generation and bootstrap.
3
+ */
4
+ /** @import { ExportRecord } from '../types.js' */
5
+
6
+ export function buildBootstrapIndex() {
7
+ return `# Memory Index\n\n_No memories yet._\n`;
8
+ }
9
+
10
+ export function createBootstrapRecords(now = Date.now()) {
11
+ return [
12
+ {
13
+ path: '_tree.md',
14
+ content: buildBootstrapIndex(),
15
+ oneLiner: 'Root index of memory filesystem',
16
+ parentPath: '',
17
+ createdAt: now,
18
+ updatedAt: now
19
+ }
20
+ ];
21
+ }
22
+
23
+ export function buildTree(files) {
24
+ const lines = ['# Memory Index', ''];
25
+
26
+ if (files.length > 0) {
27
+ for (const file of files) {
28
+ const count = file.itemCount || 0;
29
+ const updated = file.updatedAt
30
+ ? new Date(file.updatedAt).toISOString().split('T')[0]
31
+ : '';
32
+ const meta = count > 0
33
+ ? `(${count} item${count !== 1 ? 's' : ''}, updated ${updated})`
34
+ : updated ? `(updated ${updated})` : '';
35
+ lines.push(`- ${file.path} ${meta} — ${file.oneLiner}`);
36
+ }
37
+ } else {
38
+ lines.push('_No files yet._');
39
+ }
40
+
41
+ return lines.join('\n');
42
+ }
@@ -0,0 +1,125 @@
1
+ /** @import { Bullet, BulletItem, StorageBackend } from '../types.js' */
2
+ import { parseBullets, todayIsoDate } from './index.js';
3
+
4
+ class MemoryBulletIndex {
5
+ /**
6
+ * @param {StorageBackend} backend
7
+ */
8
+ constructor(backend) {
9
+ this._backend = backend;
10
+ this._initialized = false;
11
+ this._initPromise = null;
12
+ this._pathToBullets = new Map();
13
+ this._pathToUpdatedAt = new Map();
14
+ }
15
+
16
+ /** @returns {Promise<void>} */
17
+ async init() {
18
+ if (this._initialized) return;
19
+ if (this._initPromise) return this._initPromise;
20
+
21
+ this._initPromise = this._rebuild();
22
+ await this._initPromise;
23
+ }
24
+
25
+ /** @returns {Promise<void>} */
26
+ async rebuild() {
27
+ this._initialized = false;
28
+ this._initPromise = this._rebuild();
29
+ await this._initPromise;
30
+ }
31
+
32
+ async _rebuild() {
33
+ await this._backend.init();
34
+ const all = await this._backend.exportAll();
35
+ this._pathToBullets.clear();
36
+ this._pathToUpdatedAt.clear();
37
+
38
+ for (const file of all) {
39
+ if (file.path.endsWith('_tree.md')) continue;
40
+ const bullets = this._parseForIndex(file.path, file.content || '');
41
+ this._pathToBullets.set(file.path, bullets);
42
+ this._pathToUpdatedAt.set(file.path, file.updatedAt || Date.now());
43
+ }
44
+
45
+ this._initialized = true;
46
+ this._initPromise = null;
47
+ }
48
+
49
+ _parseForIndex(path, content) {
50
+ const parsed = parseBullets(content || '');
51
+ if (parsed.length > 0) return parsed;
52
+
53
+ // Lightweight fallback for legacy files: use plain lines as bullets.
54
+ const lines = String(content || '')
55
+ .split('\n')
56
+ .map((line) => line.trim())
57
+ .filter(Boolean)
58
+ .filter((line) => !line.startsWith('#'))
59
+ .filter((line) => !/^_no entries yet\._$/i.test(line))
60
+ .slice(0, 200);
61
+
62
+ const today = todayIsoDate();
63
+ return lines.map((line) => ({
64
+ text: line,
65
+ topic: path.split('/')[0] || 'general',
66
+ updatedAt: today,
67
+ expiresAt: null,
68
+ reviewAt: null,
69
+ tier: 'long_term',
70
+ status: 'active',
71
+ source: null,
72
+ confidence: null,
73
+ explicitTier: false,
74
+ explicitStatus: false,
75
+ explicitSource: false,
76
+ explicitConfidence: false,
77
+ heading: 'General',
78
+ section: 'long_term',
79
+ lineIndex: 0
80
+ }));
81
+ }
82
+
83
+ /**
84
+ * @param {string} path
85
+ * @returns {Promise<void>}
86
+ */
87
+ async refreshPath(path) {
88
+ await this.init();
89
+ if (!path || path.endsWith('_tree.md')) return;
90
+
91
+ const content = await this._backend.read(path);
92
+ if (content === null) {
93
+ this._pathToBullets.delete(path);
94
+ this._pathToUpdatedAt.delete(path);
95
+ return;
96
+ }
97
+
98
+ const bullets = this._parseForIndex(path, content);
99
+ this._pathToBullets.set(path, bullets);
100
+ this._pathToUpdatedAt.set(path, Date.now());
101
+ }
102
+
103
+ /**
104
+ * @param {string[]} paths
105
+ * @returns {BulletItem[]}
106
+ */
107
+ getBulletsForPaths(paths) {
108
+ if (!this._initialized) return [];
109
+ const items = [];
110
+ for (const path of paths || []) {
111
+ const bullets = this._pathToBullets.get(path);
112
+ if (!bullets || bullets.length === 0) continue;
113
+ for (const bullet of bullets) {
114
+ items.push({
115
+ path,
116
+ bullet,
117
+ fileUpdatedAt: this._pathToUpdatedAt.get(path) || 0
118
+ });
119
+ }
120
+ }
121
+ return items;
122
+ }
123
+ }
124
+
125
+ export { MemoryBulletIndex };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Bullet compaction — deduplication, tier assignment, strength-based ordering.
3
+ *
4
+ * Strength ordering uses source > confidence > recency:
5
+ * user_statement > assistant_summary > system > inference
6
+ * high > medium > low
7
+ * newer > older
8
+ */
9
+ /** @import { Bullet, CompactionResult, CompactBulletsOptions, Confidence, Source } from '../types.js' */
10
+ import {
11
+ ensureBulletMetadata,
12
+ normalizeFactText,
13
+ normalizeTier,
14
+ normalizeStatus,
15
+ normalizeSource,
16
+ normalizeConfidence,
17
+ normalizeTierToSection,
18
+ inferStatusFromSection,
19
+ isExpiredBullet,
20
+ todayIsoDate,
21
+ normalizeTopic,
22
+ defaultConfidenceForSource,
23
+ } from './normalize.js';
24
+
25
+ /**
26
+ * Compact a list of bullets: deduplicate, assign tiers, enforce limits.
27
+ * @param {Bullet[]} bullets
28
+ * @param {CompactBulletsOptions} [options]
29
+ * @returns {CompactionResult}
30
+ */
31
+ export function compactBullets(bullets, options = {}) {
32
+ const today = options.today || todayIsoDate();
33
+ const maxActivePerTopic = typeof options.maxActivePerTopic === 'number' && Number.isFinite(options.maxActivePerTopic)
34
+ ? Math.max(1, options.maxActivePerTopic)
35
+ : 24;
36
+ const defaultTopic = normalizeTopic(options.defaultTopic || 'general');
37
+
38
+ // Deduplicate by normalized text — keep the stronger/newer variant.
39
+ const dedup = new Map();
40
+ for (const original of bullets) {
41
+ const normalized = ensureBulletMetadata(original, { defaultTopic, updatedAt: today });
42
+ const key = normalizeFactText(normalized.text);
43
+ if (!key) continue;
44
+ const existing = dedup.get(key);
45
+ if (!existing || compareBulletStrength(normalized, existing) >= 0) {
46
+ dedup.set(key, normalized);
47
+ }
48
+ }
49
+
50
+ const working = [];
51
+ const longTerm = [];
52
+ const history = [];
53
+
54
+ // Group by topic, separate expired/superseded upfront.
55
+ const byTopic = new Map();
56
+ for (const bullet of dedup.values()) {
57
+ const tier = normalizeTier(bullet.tier || bullet.section || 'long_term');
58
+ const status = normalizeStatus(bullet.status || inferStatusFromSection(normalizeTierToSection(tier)));
59
+
60
+ if (tier === 'history' || status === 'superseded' || status === 'expired' || isExpiredBullet(bullet, today)) {
61
+ history.push({ ...bullet, tier: 'history', status: status === 'active' ? 'superseded' : status, section: 'history' });
62
+ continue;
63
+ }
64
+
65
+ const topic = bullet.topic || defaultTopic;
66
+ const list = byTopic.get(topic) || [];
67
+ list.push({ ...bullet, topic, tier, status, section: normalizeTierToSection(tier) });
68
+ byTopic.set(topic, list);
69
+ }
70
+
71
+ // Sort by strength, enforce per-topic limit, overflow to history.
72
+ for (const [topic, list] of byTopic.entries()) {
73
+ list.sort((a, b) => compareBulletStrength(b, a));
74
+ for (const item of list.slice(0, maxActivePerTopic)) {
75
+ if (item.tier === 'working') {
76
+ working.push({ ...item, topic, tier: 'working', status: item.status || 'active', section: 'working' });
77
+ } else {
78
+ longTerm.push({ ...item, topic, tier: 'long_term', status: item.status || 'active', section: 'long_term' });
79
+ }
80
+ }
81
+ for (const item of list.slice(maxActivePerTopic)) {
82
+ history.push({ ...item, topic, tier: 'history', status: 'superseded', section: 'history' });
83
+ }
84
+ }
85
+
86
+ const byRecency = (a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || '');
87
+ working.sort(byRecency);
88
+ longTerm.sort(byRecency);
89
+ history.sort(byRecency);
90
+
91
+ return { working, longTerm, history, active: [...working, ...longTerm], archive: history };
92
+ }
93
+
94
+ function compareBulletStrength(a, b) {
95
+ const aSource = normalizeSource(a?.source, 'user_statement');
96
+ const bSource = normalizeSource(b?.source, 'user_statement');
97
+ const aConf = normalizeConfidence(a?.confidence, defaultConfidenceForSource(aSource));
98
+ const bConf = normalizeConfidence(b?.confidence, defaultConfidenceForSource(bSource));
99
+
100
+ const srcRank = { inference: 0, system: 1, assistant_summary: 2, user_statement: 3 };
101
+ const srcDiff = (srcRank[aSource] ?? 0) - (srcRank[bSource] ?? 0);
102
+ if (srcDiff !== 0) return srcDiff;
103
+
104
+ const confRank = { low: 0, medium: 1, high: 2 };
105
+ const confDiff = (confRank[aConf] ?? 1) - (confRank[bConf] ?? 1);
106
+ if (confDiff !== 0) return confDiff;
107
+
108
+ return String(a?.updatedAt || '').localeCompare(String(b?.updatedAt || ''));
109
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Barrel re-export for all bullet utilities.
3
+ *
4
+ * @typedef {import('../types.js').Tier} Tier
5
+ * @typedef {import('../types.js').Status} Status
6
+ * @typedef {import('../types.js').Source} Source
7
+ * @typedef {import('../types.js').Confidence} Confidence
8
+ * @typedef {import('../types.js').Bullet} Bullet
9
+ * @typedef {import('../types.js').EnsureBulletMetadataOptions} EnsureBulletMetadataOptions
10
+ * @typedef {import('../types.js').CompactionResult} CompactionResult
11
+ * @typedef {import('../types.js').CompactBulletsOptions} CompactBulletsOptions
12
+ */
13
+ export * from './normalize.js';
14
+ export * from './parser.js';
15
+ export * from './scoring.js';
16
+ export * from './compaction.js';