crbro-memory 2.10.0 → 2.11.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 (205) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +256 -465
  3. package/SECURITY.md +114 -12
  4. package/bin/crbro.mjs +539 -300
  5. package/dist/build-id.json +10 -0
  6. package/dist/cli/common.js +211 -0
  7. package/dist/cli/export.js +522 -0
  8. package/dist/cli/import.js +683 -0
  9. package/dist/cli/markdown.js +313 -0
  10. package/dist/cli/mcpclient.js +107 -0
  11. package/dist/cli/serve.js +453 -0
  12. package/dist/cli/view.js +358 -0
  13. package/dist/cli/viewer-page.js +378 -0
  14. package/dist/daemon/daemon.js +119 -11
  15. package/dist/daemon/endpoint.js +222 -21
  16. package/dist/daemon/lines.js +102 -13
  17. package/dist/daemon/proxy.js +480 -125
  18. package/dist/daemon/surface.js +116 -0
  19. package/dist/daemon/trace.js +73 -0
  20. package/dist/engine/atrest.js +132 -0
  21. package/dist/engine/backup.js +139 -19
  22. package/dist/engine/brain.js +22 -6
  23. package/dist/engine/codexconfig.js +476 -0
  24. package/dist/engine/cortex.js +667 -232
  25. package/dist/engine/doctor.js +312 -14
  26. package/dist/engine/heat.js +81 -37
  27. package/dist/engine/hippocampus.js +18 -7
  28. package/dist/engine/hookrefresh.js +296 -0
  29. package/dist/engine/hookverify.js +15 -3
  30. package/dist/engine/installer.js +1022 -0
  31. package/dist/engine/installio.js +279 -0
  32. package/dist/engine/keychain.js +721 -162
  33. package/dist/engine/maintenance.js +355 -87
  34. package/dist/engine/modinstall.js +98 -45
  35. package/dist/engine/prefrontal.js +29 -6
  36. package/dist/engine/quarantine.js +204 -0
  37. package/dist/engine/secrets.js +41 -5
  38. package/dist/engine/shelf.js +25 -3
  39. package/dist/engine/synapses.js +183 -80
  40. package/dist/engine/triggers.js +141 -11
  41. package/dist/index.js +37 -3
  42. package/dist/mcp-surface.json +1 -0
  43. package/dist/miner/extractor.js +64 -17
  44. package/dist/miner/scheduler.js +144 -68
  45. package/dist/search/index.js +797 -176
  46. package/dist/search/semantic.js +208 -18
  47. package/dist/search/tokenize.js +44 -0
  48. package/dist/server.js +583 -373
  49. package/dist/sync/git.js +203 -72
  50. package/dist/sync/space.js +100 -25
  51. package/dist/sync/validate.js +204 -0
  52. package/dist/utils/budget.js +15 -2
  53. package/dist/utils/fs.js +40 -6
  54. package/dist/utils/ids.js +128 -14
  55. package/dist/utils/lock.js +94 -7
  56. package/hooks/crbro-codex.mjs +284 -0
  57. package/hooks/crbro-guard.mjs +281 -173
  58. package/hooks/crbro-hooks.shipped.json +20 -0
  59. package/hooks/crbro-lifecycle.mjs +16 -4
  60. package/hooks/crbro-subagent.mjs +180 -180
  61. package/package.json +9 -5
  62. package/dist/daemon/daemon.d.ts +0 -25
  63. package/dist/daemon/daemon.d.ts.map +0 -1
  64. package/dist/daemon/daemon.js.map +0 -1
  65. package/dist/daemon/endpoint.d.ts +0 -66
  66. package/dist/daemon/endpoint.d.ts.map +0 -1
  67. package/dist/daemon/endpoint.js.map +0 -1
  68. package/dist/daemon/lines.d.ts +0 -55
  69. package/dist/daemon/lines.d.ts.map +0 -1
  70. package/dist/daemon/lines.js.map +0 -1
  71. package/dist/daemon/proxy.d.ts +0 -55
  72. package/dist/daemon/proxy.d.ts.map +0 -1
  73. package/dist/daemon/proxy.js.map +0 -1
  74. package/dist/engine/backup.d.ts +0 -68
  75. package/dist/engine/backup.d.ts.map +0 -1
  76. package/dist/engine/backup.js.map +0 -1
  77. package/dist/engine/brain.d.ts +0 -79
  78. package/dist/engine/brain.d.ts.map +0 -1
  79. package/dist/engine/brain.js.map +0 -1
  80. package/dist/engine/cortex.d.ts +0 -682
  81. package/dist/engine/cortex.d.ts.map +0 -1
  82. package/dist/engine/cortex.js.map +0 -1
  83. package/dist/engine/dates.d.ts +0 -28
  84. package/dist/engine/dates.d.ts.map +0 -1
  85. package/dist/engine/dates.js.map +0 -1
  86. package/dist/engine/doctor.d.ts +0 -93
  87. package/dist/engine/doctor.d.ts.map +0 -1
  88. package/dist/engine/doctor.js.map +0 -1
  89. package/dist/engine/evidence.d.ts +0 -26
  90. package/dist/engine/evidence.d.ts.map +0 -1
  91. package/dist/engine/evidence.js.map +0 -1
  92. package/dist/engine/heat.d.ts +0 -28
  93. package/dist/engine/heat.d.ts.map +0 -1
  94. package/dist/engine/heat.js.map +0 -1
  95. package/dist/engine/hippocampus.d.ts +0 -60
  96. package/dist/engine/hippocampus.d.ts.map +0 -1
  97. package/dist/engine/hippocampus.js.map +0 -1
  98. package/dist/engine/hookverify.d.ts +0 -42
  99. package/dist/engine/hookverify.d.ts.map +0 -1
  100. package/dist/engine/hookverify.js.map +0 -1
  101. package/dist/engine/keychain.d.ts +0 -32
  102. package/dist/engine/keychain.d.ts.map +0 -1
  103. package/dist/engine/keychain.js.map +0 -1
  104. package/dist/engine/maintenance.d.ts +0 -218
  105. package/dist/engine/maintenance.d.ts.map +0 -1
  106. package/dist/engine/maintenance.js.map +0 -1
  107. package/dist/engine/modinstall.d.ts +0 -215
  108. package/dist/engine/modinstall.d.ts.map +0 -1
  109. package/dist/engine/modinstall.js.map +0 -1
  110. package/dist/engine/postmortem.d.ts +0 -81
  111. package/dist/engine/postmortem.d.ts.map +0 -1
  112. package/dist/engine/postmortem.js.map +0 -1
  113. package/dist/engine/prefrontal.d.ts +0 -64
  114. package/dist/engine/prefrontal.d.ts.map +0 -1
  115. package/dist/engine/prefrontal.js.map +0 -1
  116. package/dist/engine/projects.d.ts +0 -20
  117. package/dist/engine/projects.d.ts.map +0 -1
  118. package/dist/engine/projects.js.map +0 -1
  119. package/dist/engine/secrets.d.ts +0 -30
  120. package/dist/engine/secrets.d.ts.map +0 -1
  121. package/dist/engine/secrets.js.map +0 -1
  122. package/dist/engine/shelf.d.ts +0 -155
  123. package/dist/engine/shelf.d.ts.map +0 -1
  124. package/dist/engine/shelf.js.map +0 -1
  125. package/dist/engine/source.d.ts +0 -6
  126. package/dist/engine/source.d.ts.map +0 -1
  127. package/dist/engine/source.js.map +0 -1
  128. package/dist/engine/synapses.d.ts +0 -82
  129. package/dist/engine/synapses.d.ts.map +0 -1
  130. package/dist/engine/synapses.js.map +0 -1
  131. package/dist/engine/triggers.d.ts +0 -53
  132. package/dist/engine/triggers.d.ts.map +0 -1
  133. package/dist/engine/triggers.js.map +0 -1
  134. package/dist/engine/usage.d.ts +0 -61
  135. package/dist/engine/usage.d.ts.map +0 -1
  136. package/dist/engine/usage.js.map +0 -1
  137. package/dist/index.d.ts +0 -2
  138. package/dist/index.d.ts.map +0 -1
  139. package/dist/index.js.map +0 -1
  140. package/dist/miner/extractor.d.ts +0 -12
  141. package/dist/miner/extractor.d.ts.map +0 -1
  142. package/dist/miner/extractor.js.map +0 -1
  143. package/dist/miner/index.d.ts +0 -61
  144. package/dist/miner/index.d.ts.map +0 -1
  145. package/dist/miner/index.js.map +0 -1
  146. package/dist/miner/scheduler.d.ts +0 -30
  147. package/dist/miner/scheduler.d.ts.map +0 -1
  148. package/dist/miner/scheduler.js.map +0 -1
  149. package/dist/miner/types.d.ts +0 -11
  150. package/dist/miner/types.d.ts.map +0 -1
  151. package/dist/miner/types.js.map +0 -1
  152. package/dist/search/index.d.ts +0 -328
  153. package/dist/search/index.d.ts.map +0 -1
  154. package/dist/search/index.js.map +0 -1
  155. package/dist/search/quality.d.ts +0 -18
  156. package/dist/search/quality.d.ts.map +0 -1
  157. package/dist/search/quality.js.map +0 -1
  158. package/dist/search/semantic.d.ts +0 -80
  159. package/dist/search/semantic.d.ts.map +0 -1
  160. package/dist/search/semantic.js.map +0 -1
  161. package/dist/search/synonyms.d.ts +0 -2
  162. package/dist/search/synonyms.d.ts.map +0 -1
  163. package/dist/search/synonyms.js.map +0 -1
  164. package/dist/search/tokenize.d.ts +0 -39
  165. package/dist/search/tokenize.d.ts.map +0 -1
  166. package/dist/search/tokenize.js.map +0 -1
  167. package/dist/server.d.ts +0 -41
  168. package/dist/server.d.ts.map +0 -1
  169. package/dist/server.js.map +0 -1
  170. package/dist/sync/git.d.ts +0 -36
  171. package/dist/sync/git.d.ts.map +0 -1
  172. package/dist/sync/git.js.map +0 -1
  173. package/dist/sync/materialize.d.ts +0 -36
  174. package/dist/sync/materialize.d.ts.map +0 -1
  175. package/dist/sync/materialize.js.map +0 -1
  176. package/dist/sync/ops.d.ts +0 -147
  177. package/dist/sync/ops.d.ts.map +0 -1
  178. package/dist/sync/ops.js.map +0 -1
  179. package/dist/sync/space.d.ts +0 -118
  180. package/dist/sync/space.d.ts.map +0 -1
  181. package/dist/sync/space.js.map +0 -1
  182. package/dist/types/index.d.ts +0 -369
  183. package/dist/types/index.d.ts.map +0 -1
  184. package/dist/types/index.js.map +0 -1
  185. package/dist/utils/budget.d.ts +0 -44
  186. package/dist/utils/budget.d.ts.map +0 -1
  187. package/dist/utils/budget.js.map +0 -1
  188. package/dist/utils/fs.d.ts +0 -59
  189. package/dist/utils/fs.d.ts.map +0 -1
  190. package/dist/utils/fs.js.map +0 -1
  191. package/dist/utils/hash.d.ts +0 -20
  192. package/dist/utils/hash.d.ts.map +0 -1
  193. package/dist/utils/hash.js.map +0 -1
  194. package/dist/utils/ids.d.ts +0 -47
  195. package/dist/utils/ids.d.ts.map +0 -1
  196. package/dist/utils/ids.js.map +0 -1
  197. package/dist/utils/lock.d.ts +0 -13
  198. package/dist/utils/lock.d.ts.map +0 -1
  199. package/dist/utils/lock.js.map +0 -1
  200. package/dist/utils/transcripts.d.ts +0 -63
  201. package/dist/utils/transcripts.d.ts.map +0 -1
  202. package/dist/utils/transcripts.js.map +0 -1
  203. package/dist/version.d.ts +0 -26
  204. package/dist/version.d.ts.map +0 -1
  205. package/dist/version.js.map +0 -1
@@ -5,10 +5,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.Maintenance = void 0;
6
6
  const fs_js_1 = require("../utils/fs.js");
7
7
  const lock_js_1 = require("../utils/lock.js");
8
+ const quarantine_js_1 = require("./quarantine.js");
8
9
  const ops_js_1 = require("../sync/ops.js");
9
10
  const dates_js_1 = require("./dates.js");
10
11
  const tokenize_js_1 = require("../search/tokenize.js");
11
12
  const hash_js_1 = require("../utils/hash.js");
13
+ const extractor_js_1 = require("../miner/extractor.js");
12
14
  const shelf_js_1 = require("./shelf.js");
13
15
  /**
14
16
  * A bulk import, not a person: this many one-line neurons born the same day
@@ -26,6 +28,10 @@ const SPLIT_MAX_CANDIDATES = 8;
26
28
  const SPLIT_GROUPS = 5;
27
29
  const EXPIRED_SAMPLE = 10;
28
30
  const STALE_SAMPLE = 10;
31
+ /** Hygiene report: a neuron this big with no summary is worth two lines describing it. */
32
+ const SUMMARY_MISSING_MIN = 20;
33
+ /** Entry ids listed per suggested part; the count is always whole. */
34
+ const PARTITION_IDS = 50;
29
35
  /** Too common to name a subtopic. Short on purpose: the share bounds below do most of the work. */
30
36
  const SPLIT_STOP = new Set(('para como pero este esta estos estas desde hasta entre sobre cuando donde porque tambien todos todas cada '
31
37
  + 'tiene tienen hace hacer puede pueden debe deben usar solo antes despues ahora siempre nunca mismo misma nueva nuevo '
@@ -87,8 +93,14 @@ class Maintenance {
87
93
  compact_groups: [],
88
94
  compactable_neurons: 0,
89
95
  compacted_neurons: 0,
96
+ unreadable_neurons: [],
97
+ archivable_ids: [],
98
+ quarantine_pruned: 0,
99
+ quarantine_days: (0, quarantine_js_1.quarantineDays)(),
90
100
  notes: [],
91
101
  };
102
+ this.ilegibles.clear();
103
+ const compartidas = options?.sharedIds ?? new Set();
92
104
  // 0. Bring neurons back from the archive, before anything else could
93
105
  // archive them again in the same pass.
94
106
  const recuperados = new Set();
@@ -129,17 +141,22 @@ class Maintenance {
129
141
  // searchable and has no restore path. One routine call would have swallowed
130
142
  // 87% of the memory. Heat decays with time, so a brain that is merely old
131
143
  // looks identical to a brain that is worthless.
132
- report.archivable_neurons = await this.countColdNeurons(recuperados);
144
+ // Protocols are injected at every boot and shared neurons come back on
145
+ // the next sync: neither is ever archived (2.11), however cold it looks.
146
+ const frias = await this.coldNeuronIds(recuperados, compartidas);
147
+ report.archivable_neurons = frias.length;
148
+ report.archivable_ids = dryRun ? frias : frias.slice(0, 20);
133
149
  if (options?.archive && !dryRun) {
134
- report.archived_neurons = await this.archiveColdNeurons(recuperados);
150
+ report.archived_neurons = await this.archiveColdNeurons(recuperados, compartidas);
135
151
  report.notes.push(`Archived ${report.archived_neurons} cold neurons to ${this.brain.paths.archives}. ` +
136
- 'They are no longer searchable; pass unarchive:[ids] (or "all") to bring them back.');
152
+ 'They are no longer searchable; their synapses and connections are kept. Pass unarchive:[ids] (or "all") to bring them back.');
137
153
  }
138
154
  else if (report.archivable_neurons > 0) {
139
- report.notes.push(`${report.archivable_neurons} neurons are cold enough to archive. Nothing was archived: ` +
155
+ report.notes.push(`${report.archivable_neurons} neurons are cold enough to archive (archivable_ids${dryRun ? '' : ', first 20; dry_run lists all'}). Nothing was archived: ` +
140
156
  'pass archive:true if you have reviewed the list and really want them out of the way.');
141
157
  }
142
- report.archives_count = (await (0, fs_js_1.listJSONFiles)(this.brain.paths.archives)).length;
158
+ const archivadas = await (0, fs_js_1.listJSONFiles)(this.brain.paths.archives);
159
+ report.archives_count = archivadas.length;
143
160
  // 2b. Miner leftovers.
144
161
  //
145
162
  // Early versions of the miner recorded "Referenced in: <file>" for every
@@ -153,28 +170,46 @@ class Maintenance {
153
170
  'Neurons left with nothing in them were not deleted; review them with crbro_inspect view=neurons.');
154
171
  }
155
172
  else if (report.boilerplate_facts > 0) {
156
- report.notes.push(`${report.boilerplate_facts} contentless miner fact(s) ("Referenced in: ...") are taking up ` +
173
+ report.notes.push(`${report.boilerplate_facts} contentless miner fact(s) ("Referenced in: ...", checklist lines like "- x Navigate to ...", bare links) are taking up ` +
157
174
  'space and index slots. Pass purge_boilerplate:true to remove them.');
158
175
  }
159
176
  // 2c. Locks abandoned by a process that died mid-write.
160
177
  if (!dryRun) {
161
178
  report.stale_locks_swept = await (0, lock_js_1.sweepStaleLocks)(this.brain.paths.cortex);
162
179
  }
163
- // 3. Decay and prune weak synapses
180
+ // 3. Decay and prune weak synapses, by elapsed time. The synapses of an
181
+ // archived neuron are left alone: it is out of the way, not gone.
182
+ const enArchivo = new Set(archivadas);
164
183
  if (!dryRun) {
165
- report.pruned_synapses = await this.synapses.decay(0.05);
184
+ report.pruned_synapses = await this.synapses.decay(0.05, { skipNodes: enArchivo });
185
+ }
186
+ else {
187
+ report.prunable_synapses = await this.synapses.decay(0.05, { dryRun: true, skipNodes: enArchivo });
188
+ }
189
+ // 3b. Quarantine retention (2.11): copies older than CRBRO_QUARANTINE_DAYS.
190
+ try {
191
+ const q = await (0, quarantine_js_1.pruneQuarantine)(this.brain.paths.quarantine, { days: report.quarantine_days, dryRun });
192
+ report.quarantine_pruned = q.pruned;
193
+ if (q.pruned > 0) {
194
+ report.notes.push(dryRun
195
+ ? `${q.pruned} quarantine cop${q.pruned === 1 ? 'y is' : 'ies are'} older than ${q.days} days and would be removed (CRBRO_QUARANTINE_DAYS; 0 keeps them forever).`
196
+ : `Removed ${q.pruned} quarantine cop${q.pruned === 1 ? 'y' : 'ies'} older than ${q.days} days (CRBRO_QUARANTINE_DAYS; 0 keeps them forever).`);
197
+ }
198
+ }
199
+ catch (err) {
200
+ report.notes.push(`Quarantine retention skipped: ${err.message}`);
166
201
  }
167
202
  // 4. Integrity check
168
- report.integrity_issues = await this.checkIntegrity();
203
+ report.integrity_issues = await this.checkIntegrity(enArchivo);
169
204
  report.repairable = report.integrity_issues.length;
170
205
  // 4b. Repair what the check found. Opt-in and never in a dry run.
171
206
  if (options?.repair && !dryRun) {
172
- report.repairs = await this.repair();
207
+ report.repairs = await this.repair(enArchivo);
173
208
  report.repaired = report.repairs.length;
174
209
  if (report.repaired > 0) {
175
210
  report.notes.push(`Repaired ${report.repaired} integrity issue(s); see repairs[].`);
176
211
  // What is left after the fixes, so the report does not list issues that are gone.
177
- report.integrity_issues = await this.checkIntegrity();
212
+ report.integrity_issues = await this.checkIntegrity(enArchivo);
178
213
  }
179
214
  }
180
215
  else if (report.repairable > 0) {
@@ -220,10 +255,16 @@ class Maintenance {
220
255
  // 4d. What the brain still says as current although its own words put a
221
256
  // date on it, and what has outgrown a single read. Read-only, both: what
222
257
  // to do with a line is a judgement, and maintenance does not make it.
223
- const revision = await this.reviewEntries();
258
+ const revision = await this.reviewEntries({ hygiene: dryRun });
224
259
  report.expired_entries = revision.expired;
225
260
  report.expired_sample = revision.sample;
226
261
  report.split_candidates = revision.splits;
262
+ if (revision.hygiene) {
263
+ // The hygiene report (2.11): read-only suggestions, dry run only.
264
+ report.split_partitions = revision.hygiene.split_partitions;
265
+ report.merge_candidates = revision.hygiene.merge_candidates;
266
+ report.summary_missing = revision.hygiene.summary_missing;
267
+ }
227
268
  if (revision.stale) {
228
269
  report.stale_entries = revision.stale.count;
229
270
  report.stale_sample = revision.stale.sample;
@@ -249,7 +290,7 @@ class Maintenance {
249
290
  // are settled and do not count.
250
291
  const sinDisparador = [];
251
292
  for (const id of await (0, fs_js_1.listJSONFiles)(this.brain.paths.cortex)) {
252
- const n = await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
293
+ const n = await this.readNeuron(id);
253
294
  if (!n || !n.debts || n.debts.length === 0)
254
295
  continue;
255
296
  const vivas = n.debts.filter(d => !n.entry_status?.[(0, ops_js_1.entryId)(d)]);
@@ -289,8 +330,37 @@ class Maintenance {
289
330
  last_consolidation: (0, fs_js_1.now)(),
290
331
  });
291
332
  }
333
+ // 8. What no step could read (2.11): one bad file used to abort the
334
+ // whole run before the integrity check could even name it.
335
+ for (const id of this.heatEngine.lastUnreadable)
336
+ this.ilegibles.add(id);
337
+ for (const id of this.ilegibles) {
338
+ try {
339
+ await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
340
+ continue;
341
+ }
342
+ catch { /* still unreadable */ }
343
+ report.unreadable_neurons.push({ id, path: this.brain.paths.neuron(id), error: 'does not parse as JSON', recovery: await this.cortex.recoveryHint(id) });
344
+ }
345
+ report.unreadable_neurons.sort((a, b) => (a.id < b.id ? -1 : 1));
346
+ if (report.unreadable_neurons.length > 0) {
347
+ report.notes.push(`${report.unreadable_neurons.length} neuron file(s) could not be read and were skipped by every step (unreadable_neurons, ` +
348
+ 'each with where a good copy may be). Nothing in them was changed.');
349
+ }
292
350
  return report;
293
351
  }
352
+ /** Neuron files a step met and could not parse, this run. */
353
+ ilegibles = new Set();
354
+ /** A tolerant read for the passes over the cortex: a corrupt file is noted and skipped. */
355
+ async readNeuron(id) {
356
+ try {
357
+ return await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
358
+ }
359
+ catch {
360
+ this.ilegibles.add(id);
361
+ return null;
362
+ }
363
+ }
294
364
  /**
295
365
  * Consolidate session — called at end of session.
296
366
  */
@@ -354,8 +424,15 @@ class Maintenance {
354
424
  // updateContext({}), which rewrote the file and set nothing, so boot
355
425
  // reported last_session: null forever.
356
426
  await this.prefrontal.setLastSession(log.session_id);
357
- // Recalculate heat
427
+ // Recalculate heat. Since 2.11 it re-reads only the neurons whose file
428
+ // changed since the last pass, and never rewrites a neuron for it.
358
429
  await this.heatEngine.recalculate();
430
+ // Quarantine retention (2.11). Never fatal: a consolidate must log.
431
+ let quarantinePruned = 0;
432
+ try {
433
+ quarantinePruned = (await (0, quarantine_js_1.pruneQuarantine)(this.brain.paths.quarantine, { days: (0, quarantine_js_1.quarantineDays)() })).pruned;
434
+ }
435
+ catch { /* retention is housekeeping */ }
359
436
  // Update manifest
360
437
  await this.brain.updateManifest({
361
438
  total_neurons: neuronCount,
@@ -375,82 +452,112 @@ class Maintenance {
375
452
  redacted: log.redacted,
376
453
  topics_logged: topicosLog,
377
454
  topics_unknown: desconocidos,
455
+ quarantine_pruned: quarantinePruned,
456
+ ...(this.heatEngine.lastUnreadable.length ? { unreadable_neurons: [...this.heatEngine.lastUnreadable] } : {}),
378
457
  };
379
458
  }
380
459
  // ─── Private Helpers ──────────────────────────────────────────
381
- /** "Referenced in: x.md" and "Mined from x.md" carry no information. */
382
- isBoilerplate(text) {
383
- return /^(Referenced in|Mined from):?\s/i.test((text || '').trim());
460
+ /**
461
+ * "Referenced in: x.md" and "Mined from x.md" carry no information; nor do
462
+ * the checklist lines, bare links and headings the miner stored as facts
463
+ * before 2.11 (isMinerNoise, only for facts whose source is the miner).
464
+ */
465
+ isBoilerplate(text, source) {
466
+ return /^(Referenced in|Mined from):?\s/i.test((text || '').trim()) || (source === 'miner' && (0, extractor_js_1.isMinerNoise)(text));
384
467
  }
385
468
  async countBoilerplate() {
386
469
  let n = 0;
387
470
  for (const id of await this.cortex.allIds()) {
388
- const neuron = await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
471
+ const neuron = await this.readNeuron(id);
389
472
  if (!neuron)
390
473
  continue;
391
- n += (neuron.facts || []).filter(f => this.isBoilerplate(f.text)).length;
474
+ n += (neuron.facts || []).filter(f => this.isBoilerplate(f.text, f.source)).length;
392
475
  }
393
476
  return n;
394
477
  }
395
478
  async purgeBoilerplate() {
396
479
  let removed = 0;
397
480
  for (const id of await this.cortex.allIds()) {
398
- await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(id), current => {
399
- if (!current)
400
- return null;
401
- const antes = (current.facts || []).length;
402
- current.facts = (current.facts || []).filter(f => !this.isBoilerplate(f.text));
403
- const quitados = antes - current.facts.length;
404
- if (quitados === 0)
405
- return null;
406
- removed += quitados;
407
- return current;
408
- });
481
+ try {
482
+ await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(id), async (current) => {
483
+ if (!current)
484
+ return null;
485
+ const fuera = (current.facts || []).filter(f => this.isBoilerplate(f.text, f.source));
486
+ if (fuera.length === 0)
487
+ return null;
488
+ // What leaves goes to quarantine first (2.11), like every other
489
+ // removal: purge used to be the one deletion with no way back.
490
+ const q = {
491
+ quarantine_format: 2, kind: 'entries', neuron_id: current.id, name: current.name, type: current.type,
492
+ domain: current.domain, taken_at: (0, fs_js_1.now)(), facts: fuera, decisions: [], patterns: [], preferences: [], errors: [], debts: [],
493
+ };
494
+ await (0, quarantine_js_1.writeQuarantine)(this.brain.paths.quarantine, current.id, q, 'entries');
495
+ current.facts = (current.facts || []).filter(f => !this.isBoilerplate(f.text, f.source));
496
+ removed += fuera.length;
497
+ return current;
498
+ });
499
+ }
500
+ catch (err) {
501
+ if (err instanceof fs_js_1.CorruptJSONError) {
502
+ this.ilegibles.add(id);
503
+ continue;
504
+ }
505
+ throw err;
506
+ }
409
507
  }
410
508
  if (removed > 0)
411
509
  await this.searchEngine.rebuild();
412
510
  return removed;
413
511
  }
414
- /** Cold: heat below 0.05 and untouched for more than 90 days. */
415
- isCold(neuron) {
512
+ /**
513
+ * Cold: heat below 0.05 and untouched for more than 90 days. Heat comes
514
+ * from the derived heat state when there is one (2.11), else the neuron's
515
+ * own stored value, as before.
516
+ */
517
+ isCold(neuron, heat) {
416
518
  const lastAccess = new Date(neuron.last_accessed).getTime();
417
519
  const diffDays = (Date.now() - lastAccess) / (1000 * 60 * 60 * 24);
418
- return neuron.heat < 0.05 && diffDays > 90;
520
+ return heat < 0.05 && diffDays > 90;
419
521
  }
420
- async archiveColdNeurons(skip = new Set()) {
421
- const ids = await this.cortex.allIds();
422
- let archived = 0;
423
- for (const id of ids) {
424
- if (skip.has(id))
522
+ /**
523
+ * What archive:true would move: cold, and neither a protocol (boot injects
524
+ * it every session, so "untouched" says nothing) nor shared (the next sync
525
+ * would bring it back from the team log). Sorted by id.
526
+ */
527
+ async coldNeuronIds(skip = new Set(), shared = new Set()) {
528
+ const calor = await this.cortex.heatMap();
529
+ const out = [];
530
+ for (const id of await this.cortex.allIds()) {
531
+ if (skip.has(id) || shared.has(id) || id.startsWith('protocol_'))
425
532
  continue;
426
- const neuron = await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
427
- if (!neuron)
533
+ const neuron = await this.readNeuron(id);
534
+ if (!neuron || neuron.type === 'protocol')
428
535
  continue;
429
- if (this.isCold(neuron)) {
430
- await (0, fs_js_1.moveFile)(this.brain.paths.neuron(id), `${this.brain.paths.archives}/${id}.json`);
431
- archived++;
432
- }
536
+ if (this.isCold(neuron, calor.get(id) ?? neuron.heat))
537
+ out.push(id);
433
538
  }
434
- return archived;
539
+ return out.sort();
435
540
  }
436
- async countColdNeurons(skip = new Set()) {
437
- const ids = await this.cortex.allIds();
438
- let count = 0;
439
- for (const id of ids) {
440
- if (skip.has(id))
441
- continue;
442
- const neuron = await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
443
- if (!neuron)
444
- continue;
445
- if (this.isCold(neuron))
446
- count++;
541
+ /**
542
+ * Move cold neurons to archives/. Only the neuron file moves: its synapse
543
+ * files and the neighbours' connection lists stay exactly as they are
544
+ * (2.11), and integrity and repair count archives/ as existing, so nothing
545
+ * of the neuron's links is lost and unarchive finds them where they were.
546
+ */
547
+ async archiveColdNeurons(skip = new Set(), shared = new Set()) {
548
+ let archived = 0;
549
+ for (const id of await this.coldNeuronIds(skip, shared)) {
550
+ await (0, fs_js_1.moveFile)(this.brain.paths.neuron(id), `${this.brain.paths.archives}/${id}.json`);
551
+ await this.searchEngine.removeNeuron(id).catch(() => { });
552
+ archived++;
447
553
  }
448
- return count;
554
+ return archived;
449
555
  }
450
556
  /**
451
557
  * Move neurons from archives/ back into cortex/ and index them. A neuron
452
558
  * that exists in cortex/ again is not overwritten: the archive copy stays
453
- * and the id is reported in `already`.
559
+ * and the id is reported in `already`. Its synapses never left, so it comes
560
+ * back connected.
454
561
  */
455
562
  async unarchive(which) {
456
563
  const out = { restored: [], unknown: [], already: [] };
@@ -485,25 +592,25 @@ class Maintenance {
485
592
  /**
486
593
  * What is inconsistent on disk. Every line here names something repair()
487
594
  * knows how to fix, except a corrupted neuron file, which nobody should
488
- * fix blindly.
595
+ * fix blindly. A neuron in archives/ exists: links to it are not broken.
489
596
  */
490
- async checkIntegrity() {
597
+ async checkIntegrity(archived = new Set()) {
491
598
  const issues = [];
492
599
  const ids = await this.cortex.allIds();
493
- const existe = new Set(ids);
600
+ const existe = new Set([...ids, ...archived]);
494
601
  for (const id of ids) {
495
602
  let neuron;
496
603
  try {
497
604
  neuron = await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
498
605
  }
499
606
  catch {
607
+ this.ilegibles.add(id);
500
608
  neuron = null;
501
609
  }
502
610
  if (!neuron) {
503
611
  issues.push(`Corrupted neuron file: ${id}`);
504
612
  continue;
505
- }
506
- // Check for broken connections
613
+ } // Check for broken connections
507
614
  for (const connId of neuron.connections || []) {
508
615
  if (!existe.has(connId)) {
509
616
  issues.push(`Broken connection: ${id} → ${connId} (target missing)`);
@@ -629,7 +736,7 @@ class Maintenance {
629
736
  * that gather between 8% and 45% of them — frequent enough to be a
630
737
  * subtopic, not so frequent that they are just the neuron's own name.
631
738
  */
632
- async reviewEntries() {
739
+ async reviewEntries(opts) {
633
740
  const hoy = (0, fs_js_1.today)();
634
741
  const vencidas = [];
635
742
  // Shelf life: the same pass judges every live fact, decision and pattern.
@@ -644,14 +751,12 @@ class Maintenance {
644
751
  const enNeuronas = new Map();
645
752
  const grandes = [];
646
753
  let total = 0;
647
- for (const id of await this.cortex.allIds()) {
648
- let n;
649
- try {
650
- n = await (0, fs_js_1.readJSON)(this.brain.paths.neuron(id));
651
- }
652
- catch {
653
- continue;
654
- }
754
+ // Hygiene report (2.11), gathered in the same pass.
755
+ const sueltas1 = [];
756
+ const sinResumen = [];
757
+ const todosIds = await this.cortex.allIds();
758
+ for (const id of todosIds) {
759
+ const n = await this.readNeuron(id);
655
760
  if (!n || n.type === 'protocol')
656
761
  continue;
657
762
  const retirada = (t) => !!n.entry_status?.[(0, ops_js_1.entryId)(t)];
@@ -727,7 +832,19 @@ class Maintenance {
727
832
  for (const w of new Set(palabras.flatMap(set => [...set])))
728
833
  enNeuronas.set(w, (enNeuronas.get(w) || 0) + 1);
729
834
  if (vivas.length >= SPLIT_MIN_ENTRIES && !(n.tags || []).includes('digest')) {
730
- grandes.push({ n, vivas: vivas.length, palabras, propias });
835
+ grandes.push({ n, vivas: vivas.length, palabras, propias, eids: vivas.map(e => e.eid) });
836
+ }
837
+ if (opts?.hygiene) {
838
+ const todas = (n.facts || []).length + (n.decisions || []).length + (n.patterns || []).length
839
+ + (n.preferences || []).length + (n.errors || []).length + (n.debts || []).length;
840
+ if (todas === 1 && vivas.length === 1 && !n.map?.text && !(n.tags || []).includes('digest')) {
841
+ const t = vivas[0].text;
842
+ sueltas1.push({ neuron_id: n.id, name: n.name, domain: n.domain || 'general', created: (n.created || '').slice(0, 10),
843
+ preview: t.length > 100 ? `${t.slice(0, 100).trimEnd()}…` : t });
844
+ }
845
+ if (!(n.summary || '').trim() && vivas.length >= SUMMARY_MISSING_MIN) {
846
+ sinResumen.push({ neuron_id: n.id, name: n.name, entries: vivas.length });
847
+ }
731
848
  }
732
849
  }
733
850
  // A word found in many neurons is how this person writes ("verificado",
@@ -775,12 +892,152 @@ class Maintenance {
775
892
  }
776
893
  vencidas.sort((a, b) => (a.due < b.due ? -1 : a.due > b.due ? 1 : a.entry_id < b.entry_id ? -1 : 1));
777
894
  gordas.sort((a, b) => b.entries - a.entries);
778
- rancias.sort((a, b) => b.overdue - a.overdue || (a.entry_id < b.entry_id ? -1 : 1));
895
+ rancias.sort((a, b) => b.overdue - a.overdue || (a.entry_id < b.entry_id ? -1 : 1) || (a.neuron_id < b.neuron_id ? -1 : 1));
896
+ const sinOverdue = (r) => { const { overdue: _o, ...rest } = r; return rest; };
897
+ let hygiene;
898
+ if (opts?.hygiene) {
899
+ // A suggested partition for each split candidate: every live entry goes
900
+ // to the first group whose word it holds; the rest stays. Ids only, so
901
+ // crbro_revise move_to can act on it as it is. Nothing is moved here.
902
+ const porId = new Map(grandes.map(g => [g.n.id, g]));
903
+ const split_partitions = [];
904
+ for (const c of gordas.slice(0, SPLIT_MAX_CANDIDATES)) {
905
+ const g = porId.get(c.neuron_id);
906
+ if (!g || c.groups.length === 0)
907
+ continue;
908
+ const parts = c.groups.map(gr => ({ term: gr.term, suggested_name: `${c.name} - ${gr.term}`, entries: 0, entry_ids: [] }));
909
+ let rest = 0;
910
+ g.palabras.forEach((set, i) => {
911
+ const p = parts.find(pt => set.has(pt.term));
912
+ if (!p) {
913
+ rest++;
914
+ return;
915
+ }
916
+ p.entries++;
917
+ if (p.entry_ids.length < PARTITION_IDS)
918
+ p.entry_ids.push(g.eids[i]);
919
+ });
920
+ split_partitions.push({ neuron_id: c.neuron_id, name: c.name, entries: c.entries, parts, rest });
921
+ }
922
+ // Ids made with their prefix twice by 2.10 from an id-shaped topic.
923
+ const existentes = new Set(todosIds);
924
+ const doubled_prefix = todosIds
925
+ .map(id => ({ id, m: id.match(/^(project|tech|lang|person|domain|process|protocol)_\1_(.+)$/) }))
926
+ .filter(x => x.m)
927
+ .map(x => ({ neuron_id: x.id, meant: `${x.m[1]}_${x.m[2]}`, meant_exists: existentes.has(`${x.m[1]}_${x.m[2]}`) }));
928
+ sueltas1.sort((a, b) => (a.created < b.created ? -1 : a.created > b.created ? 1 : a.neuron_id < b.neuron_id ? -1 : 1));
929
+ sinResumen.sort((a, b) => b.entries - a.entries || (a.neuron_id < b.neuron_id ? -1 : 1));
930
+ hygiene = {
931
+ split_partitions,
932
+ merge_candidates: { single_entry: sueltas1.slice(0, 30), single_entry_total: sueltas1.length, doubled_prefix },
933
+ summary_missing: { total: sinResumen.length, neurons: sinResumen.slice(0, 20) },
934
+ };
935
+ }
779
936
  return {
780
937
  expired: vencidas.length, sample: vencidas.slice(0, EXPIRED_SAMPLE), splits: gordas.slice(0, SPLIT_MAX_CANDIDATES),
781
- ...(vida ? { stale: { count: rancias.length, sample: rancias.slice(0, STALE_SAMPLE).map(({ overdue: _o, ...r }) => r) } } : {}),
938
+ ...(vida ? { stale: {
939
+ count: rancias.length,
940
+ sample: rancias.slice(0, STALE_SAMPLE).map(sinOverdue),
941
+ ...(opts?.staleAll ? { all: rancias.map(sinOverdue) } : {}),
942
+ } } : {}),
943
+ ...(hygiene ? { hygiene } : {}),
782
944
  };
783
945
  }
946
+ /**
947
+ * Every stale entry, a page at a time, most overdue first — the list
948
+ * stale_sample only ever showed ten of (2.11). Read-only. `neuronId`
949
+ * narrows it to one neuron.
950
+ */
951
+ async listStale(options) {
952
+ const r = await this.reviewEntries({ staleAll: true });
953
+ const offset = Math.max(0, Math.floor(options?.offset ?? 0));
954
+ const limit = Math.min(Math.max(Math.floor(options?.limit ?? 50), 1), 200);
955
+ if (!r.stale)
956
+ return { enabled: false, total: 0, offset, returned: 0, has_more: false, entries: [] };
957
+ const todas = (r.stale.all || []).filter(e => !options?.neuronId || e.neuron_id === options.neuronId);
958
+ const pagina = todas.slice(offset, offset + limit);
959
+ return { enabled: true, total: todas.length, offset, returned: pagina.length, has_more: offset + pagina.length < todas.length, entries: pagina };
960
+ }
961
+ /**
962
+ * Act on many stale entries at once: `verify` restarts their shelf-life
963
+ * clock (they were checked and still hold); `superseded` / `retracted`
964
+ * retires them. Entries are { neuron_id, entry_id } as listStale gives
965
+ * them; facts and decisions/patterns are told apart here, one locked write
966
+ * per neuron. Verification and retirement travel to shared spaces exactly
967
+ * as crbro_revise sends them.
968
+ */
969
+ async reviewStale(items, action, note) {
970
+ const out = { action, done: [], unmatched: [] };
971
+ const porNeurona = new Map();
972
+ for (const it of items || []) {
973
+ const nid = String(it?.neuron_id || '').trim();
974
+ const eid = String(it?.entry_id || '').trim();
975
+ if (!nid || !eid)
976
+ continue;
977
+ const l = porNeurona.get(nid) || [];
978
+ if (!l.includes(eid))
979
+ l.push(eid);
980
+ porNeurona.set(nid, l);
981
+ }
982
+ for (const [nid, eids] of porNeurona) {
983
+ const n = await this.cortex.peek(nid);
984
+ if (!n) {
985
+ for (const e of eids)
986
+ out.unmatched.push({ neuron_id: nid, entry_id: e, why: 'no such neuron (or unreadable)' });
987
+ continue;
988
+ }
989
+ const factIds = new Set((n.facts || []).map(f => (f.id || (0, hash_js_1.factId)(f.text))));
990
+ const entradas = new Map();
991
+ for (const d of n.decisions || []) {
992
+ entradas.set((0, ops_js_1.entryId)(d.text || ''), d.text || '');
993
+ if (d.id)
994
+ entradas.set(d.id, d.text || '');
995
+ }
996
+ for (const p of n.patterns || [])
997
+ entradas.set((0, ops_js_1.entryId)(p), p);
998
+ const facts = eids.filter(e => factIds.has(e));
999
+ const entries = eids.filter(e => !factIds.has(e) && entradas.has(e));
1000
+ for (const e of eids) {
1001
+ if (!factIds.has(e) && !entradas.has(e))
1002
+ out.unmatched.push({ neuron_id: nid, entry_id: e, why: 'not a fact, decision or pattern of this neuron' });
1003
+ }
1004
+ if (action === 'verify') {
1005
+ // Unspecified evidence, as crbro_revise status=verified without a reference.
1006
+ const r = await this.cortex.verify(nid, { facts, entries });
1007
+ const hechos = new Set(r.verified);
1008
+ for (const e of [...facts, ...entries]) {
1009
+ const id = entries.includes(e) ? (0, ops_js_1.entryId)(entradas.get(e)) : e;
1010
+ if (hechos.has(id) || hechos.has(e))
1011
+ out.done.push({ neuron_id: nid, entry_id: e });
1012
+ else
1013
+ out.unmatched.push({ neuron_id: nid, entry_id: e, why: 'retired, or not verifiable' });
1014
+ }
1015
+ continue;
1016
+ }
1017
+ if (facts.length) {
1018
+ const r = await this.cortex.revise(nid, facts, { status: action, note });
1019
+ const sin = new Set(r.unmatched);
1020
+ for (const e of facts) {
1021
+ if (sin.has(e))
1022
+ out.unmatched.push({ neuron_id: nid, entry_id: e, why: 'already retired' });
1023
+ else
1024
+ out.done.push({ neuron_id: nid, entry_id: e });
1025
+ }
1026
+ }
1027
+ if (entries.length) {
1028
+ const textos = entries.map(e => entradas.get(e));
1029
+ const r = await this.cortex.retireEntries(nid, textos, { status: action, note });
1030
+ const sin = new Set(r.unmatched);
1031
+ entries.forEach((e, i) => {
1032
+ if (sin.has(textos[i]))
1033
+ out.unmatched.push({ neuron_id: nid, entry_id: e, why: 'already retired' });
1034
+ else
1035
+ out.done.push({ neuron_id: nid, entry_id: e });
1036
+ });
1037
+ }
1038
+ }
1039
+ return out;
1040
+ }
784
1041
  /**
785
1042
  * Count the undated patterns, preferences, errors and debts, and — when
786
1043
  * `write` — date the ones whose own text says when they happened.
@@ -854,10 +1111,12 @@ class Maintenance {
854
1111
  * with a missing end, stale entry_dates / entry_status keys, manifest
855
1112
  * counters. One line per fix. Corrupted files are left for a human.
856
1113
  */
857
- async repair() {
1114
+ async repair(archived = new Set()) {
858
1115
  const done = [];
859
1116
  const ids = await this.cortex.allIds();
860
- const existe = new Set(ids);
1117
+ // archives/ counts as existing (2.11): repair used to delete for good the
1118
+ // synapses and connections of every archived neuron.
1119
+ const existe = new Set([...ids, ...archived]);
861
1120
  // Synapses first: an orphan synapse also shows up as a dangling
862
1121
  // connection on its surviving end, and disconnect() cleans both.
863
1122
  for (const sid of await (0, fs_js_1.listJSONFiles)(this.brain.paths.synapses)) {
@@ -873,19 +1132,28 @@ class Maintenance {
873
1132
  const faltan = synapse.nodes.filter(n => !existe.has(n));
874
1133
  if (faltan.length === 0)
875
1134
  continue;
1135
+ // A copy first, like every other removal (2.11).
1136
+ await (0, quarantine_js_1.writeQuarantine)(this.brain.paths.quarantine, synapse.id || sid, synapse, 'whole');
876
1137
  await (0, fs_js_1.deleteJSON)(this.brain.paths.synapse(sid));
877
1138
  for (const node of synapse.nodes) {
878
- if (!existe.has(node))
1139
+ if (!existe.has(node) || archived.has(node))
879
1140
  continue;
880
- await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(node), current => {
881
- if (!current)
882
- return null;
883
- const otro = synapse.nodes[0] === node ? synapse.nodes[1] : synapse.nodes[0];
884
- if (!(current.connections || []).includes(otro))
885
- return null;
886
- current.connections = current.connections.filter(c => c !== otro);
887
- return current;
888
- });
1141
+ try {
1142
+ await (0, fs_js_1.updateJSON)(this.brain.paths.neuron(node), current => {
1143
+ if (!current)
1144
+ return null;
1145
+ const otro = synapse.nodes[0] === node ? synapse.nodes[1] : synapse.nodes[0];
1146
+ if (!(current.connections || []).includes(otro))
1147
+ return null;
1148
+ current.connections = current.connections.filter(c => c !== otro);
1149
+ return current;
1150
+ });
1151
+ }
1152
+ catch (err) {
1153
+ if (!(err instanceof fs_js_1.CorruptJSONError))
1154
+ throw err;
1155
+ this.ilegibles.add(node);
1156
+ }
889
1157
  }
890
1158
  done.push(`Deleted orphan synapse ${sid} (missing: ${faltan.join(', ')})`);
891
1159
  }