crbro-memory 2.9.1 → 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 (197) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +256 -456
  3. package/SECURITY.md +135 -8
  4. package/bin/crbro.mjs +555 -299
  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 +143 -19
  22. package/dist/engine/brain.js +22 -6
  23. package/dist/engine/codexconfig.js +476 -0
  24. package/dist/engine/cortex.js +762 -311
  25. package/dist/engine/doctor.js +676 -0
  26. package/dist/engine/evidence.js +171 -0
  27. package/dist/engine/heat.js +81 -37
  28. package/dist/engine/hippocampus.js +50 -36
  29. package/dist/engine/hookrefresh.js +296 -0
  30. package/dist/engine/hookverify.js +15 -3
  31. package/dist/engine/installer.js +1022 -0
  32. package/dist/engine/installio.js +279 -0
  33. package/dist/engine/keychain.js +735 -144
  34. package/dist/engine/maintenance.js +355 -87
  35. package/dist/engine/modinstall.js +98 -45
  36. package/dist/engine/prefrontal.js +126 -69
  37. package/dist/engine/projects.js +80 -0
  38. package/dist/engine/quarantine.js +204 -0
  39. package/dist/engine/secrets.js +41 -5
  40. package/dist/engine/shelf.js +34 -11
  41. package/dist/engine/synapses.js +183 -80
  42. package/dist/engine/triggers.js +141 -11
  43. package/dist/index.js +37 -3
  44. package/dist/mcp-surface.json +1 -0
  45. package/dist/miner/extractor.js +64 -17
  46. package/dist/miner/scheduler.js +144 -68
  47. package/dist/search/index.js +810 -176
  48. package/dist/search/quality.js +31 -0
  49. package/dist/search/semantic.js +208 -18
  50. package/dist/search/tokenize.js +44 -0
  51. package/dist/server.js +649 -343
  52. package/dist/sync/git.js +203 -72
  53. package/dist/sync/materialize.js +33 -8
  54. package/dist/sync/space.js +286 -101
  55. package/dist/sync/validate.js +204 -0
  56. package/dist/utils/budget.js +15 -2
  57. package/dist/utils/fs.js +40 -6
  58. package/dist/utils/ids.js +128 -14
  59. package/dist/utils/lock.js +219 -52
  60. package/hooks/crbro-codex.mjs +284 -0
  61. package/hooks/crbro-guard.mjs +281 -173
  62. package/hooks/crbro-hooks.shipped.json +20 -0
  63. package/hooks/crbro-lifecycle.mjs +16 -4
  64. package/hooks/crbro-subagent.mjs +180 -180
  65. package/package.json +12 -8
  66. package/dist/daemon/daemon.d.ts +0 -25
  67. package/dist/daemon/daemon.d.ts.map +0 -1
  68. package/dist/daemon/daemon.js.map +0 -1
  69. package/dist/daemon/endpoint.d.ts +0 -66
  70. package/dist/daemon/endpoint.d.ts.map +0 -1
  71. package/dist/daemon/endpoint.js.map +0 -1
  72. package/dist/daemon/lines.d.ts +0 -55
  73. package/dist/daemon/lines.d.ts.map +0 -1
  74. package/dist/daemon/lines.js.map +0 -1
  75. package/dist/daemon/proxy.d.ts +0 -55
  76. package/dist/daemon/proxy.d.ts.map +0 -1
  77. package/dist/daemon/proxy.js.map +0 -1
  78. package/dist/engine/backup.d.ts +0 -68
  79. package/dist/engine/backup.d.ts.map +0 -1
  80. package/dist/engine/backup.js.map +0 -1
  81. package/dist/engine/brain.d.ts +0 -79
  82. package/dist/engine/brain.d.ts.map +0 -1
  83. package/dist/engine/brain.js.map +0 -1
  84. package/dist/engine/cortex.d.ts +0 -670
  85. package/dist/engine/cortex.d.ts.map +0 -1
  86. package/dist/engine/cortex.js.map +0 -1
  87. package/dist/engine/dates.d.ts +0 -28
  88. package/dist/engine/dates.d.ts.map +0 -1
  89. package/dist/engine/dates.js.map +0 -1
  90. package/dist/engine/heat.d.ts +0 -28
  91. package/dist/engine/heat.d.ts.map +0 -1
  92. package/dist/engine/heat.js.map +0 -1
  93. package/dist/engine/hippocampus.d.ts +0 -60
  94. package/dist/engine/hippocampus.d.ts.map +0 -1
  95. package/dist/engine/hippocampus.js.map +0 -1
  96. package/dist/engine/hookverify.d.ts +0 -42
  97. package/dist/engine/hookverify.d.ts.map +0 -1
  98. package/dist/engine/hookverify.js.map +0 -1
  99. package/dist/engine/keychain.d.ts +0 -32
  100. package/dist/engine/keychain.d.ts.map +0 -1
  101. package/dist/engine/keychain.js.map +0 -1
  102. package/dist/engine/maintenance.d.ts +0 -218
  103. package/dist/engine/maintenance.d.ts.map +0 -1
  104. package/dist/engine/maintenance.js.map +0 -1
  105. package/dist/engine/modinstall.d.ts +0 -215
  106. package/dist/engine/modinstall.d.ts.map +0 -1
  107. package/dist/engine/modinstall.js.map +0 -1
  108. package/dist/engine/postmortem.d.ts +0 -81
  109. package/dist/engine/postmortem.d.ts.map +0 -1
  110. package/dist/engine/postmortem.js.map +0 -1
  111. package/dist/engine/prefrontal.d.ts +0 -61
  112. package/dist/engine/prefrontal.d.ts.map +0 -1
  113. package/dist/engine/prefrontal.js.map +0 -1
  114. package/dist/engine/secrets.d.ts +0 -30
  115. package/dist/engine/secrets.d.ts.map +0 -1
  116. package/dist/engine/secrets.js.map +0 -1
  117. package/dist/engine/shelf.d.ts +0 -156
  118. package/dist/engine/shelf.d.ts.map +0 -1
  119. package/dist/engine/shelf.js.map +0 -1
  120. package/dist/engine/source.d.ts +0 -6
  121. package/dist/engine/source.d.ts.map +0 -1
  122. package/dist/engine/source.js.map +0 -1
  123. package/dist/engine/synapses.d.ts +0 -82
  124. package/dist/engine/synapses.d.ts.map +0 -1
  125. package/dist/engine/synapses.js.map +0 -1
  126. package/dist/engine/triggers.d.ts +0 -53
  127. package/dist/engine/triggers.d.ts.map +0 -1
  128. package/dist/engine/triggers.js.map +0 -1
  129. package/dist/engine/usage.d.ts +0 -61
  130. package/dist/engine/usage.d.ts.map +0 -1
  131. package/dist/engine/usage.js.map +0 -1
  132. package/dist/index.d.ts +0 -2
  133. package/dist/index.d.ts.map +0 -1
  134. package/dist/index.js.map +0 -1
  135. package/dist/miner/extractor.d.ts +0 -12
  136. package/dist/miner/extractor.d.ts.map +0 -1
  137. package/dist/miner/extractor.js.map +0 -1
  138. package/dist/miner/index.d.ts +0 -61
  139. package/dist/miner/index.d.ts.map +0 -1
  140. package/dist/miner/index.js.map +0 -1
  141. package/dist/miner/scheduler.d.ts +0 -30
  142. package/dist/miner/scheduler.d.ts.map +0 -1
  143. package/dist/miner/scheduler.js.map +0 -1
  144. package/dist/miner/types.d.ts +0 -11
  145. package/dist/miner/types.d.ts.map +0 -1
  146. package/dist/miner/types.js.map +0 -1
  147. package/dist/search/index.d.ts +0 -323
  148. package/dist/search/index.d.ts.map +0 -1
  149. package/dist/search/index.js.map +0 -1
  150. package/dist/search/semantic.d.ts +0 -80
  151. package/dist/search/semantic.d.ts.map +0 -1
  152. package/dist/search/semantic.js.map +0 -1
  153. package/dist/search/synonyms.d.ts +0 -2
  154. package/dist/search/synonyms.d.ts.map +0 -1
  155. package/dist/search/synonyms.js.map +0 -1
  156. package/dist/search/tokenize.d.ts +0 -39
  157. package/dist/search/tokenize.d.ts.map +0 -1
  158. package/dist/search/tokenize.js.map +0 -1
  159. package/dist/server.d.ts +0 -41
  160. package/dist/server.d.ts.map +0 -1
  161. package/dist/server.js.map +0 -1
  162. package/dist/sync/git.d.ts +0 -36
  163. package/dist/sync/git.d.ts.map +0 -1
  164. package/dist/sync/git.js.map +0 -1
  165. package/dist/sync/materialize.d.ts +0 -35
  166. package/dist/sync/materialize.d.ts.map +0 -1
  167. package/dist/sync/materialize.js.map +0 -1
  168. package/dist/sync/ops.d.ts +0 -142
  169. package/dist/sync/ops.d.ts.map +0 -1
  170. package/dist/sync/ops.js.map +0 -1
  171. package/dist/sync/space.d.ts +0 -116
  172. package/dist/sync/space.d.ts.map +0 -1
  173. package/dist/sync/space.js.map +0 -1
  174. package/dist/types/index.d.ts +0 -346
  175. package/dist/types/index.d.ts.map +0 -1
  176. package/dist/types/index.js.map +0 -1
  177. package/dist/utils/budget.d.ts +0 -44
  178. package/dist/utils/budget.d.ts.map +0 -1
  179. package/dist/utils/budget.js.map +0 -1
  180. package/dist/utils/fs.d.ts +0 -59
  181. package/dist/utils/fs.d.ts.map +0 -1
  182. package/dist/utils/fs.js.map +0 -1
  183. package/dist/utils/hash.d.ts +0 -20
  184. package/dist/utils/hash.d.ts.map +0 -1
  185. package/dist/utils/hash.js.map +0 -1
  186. package/dist/utils/ids.d.ts +0 -47
  187. package/dist/utils/ids.d.ts.map +0 -1
  188. package/dist/utils/ids.js.map +0 -1
  189. package/dist/utils/lock.d.ts +0 -11
  190. package/dist/utils/lock.d.ts.map +0 -1
  191. package/dist/utils/lock.js.map +0 -1
  192. package/dist/utils/transcripts.d.ts +0 -63
  193. package/dist/utils/transcripts.d.ts.map +0 -1
  194. package/dist/utils/transcripts.js.map +0 -1
  195. package/dist/version.d.ts +0 -26
  196. package/dist/version.d.ts.map +0 -1
  197. package/dist/version.js.map +0 -1
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ // ─── Quarantine: the copy kept before anything is removed ────────
3
+ //
4
+ // Every destructive path (forget, forget entire, merge_into, compact, a day
5
+ // log forgotten) writes here first, so a removal can be undone. Until 2.11 it
6
+ // grew without limit and in clear: 824 files and 5.3 MB on the reference
7
+ // brain, a whole 1 MB neuron copied for every batch of a split, including the
8
+ // credentials somebody forgot on purpose. Now:
9
+ //
10
+ // - forgetting SOME entries keeps those entries and what restore needs to
11
+ // put them back (dates, retirements, provenance, checks), not the neuron;
12
+ // - copies older than CRBRO_QUARANTINE_DAYS (default 180; 0 keeps them
13
+ // forever) are pruned by consolidate and maintenance, which say how many;
14
+ // - with CRBRO_ENCRYPT_AT_REST=1 new copies are sealed (atrest.ts).
15
+ //
16
+ // File names: `<id>.<stamp>.json` (a whole neuron or log, the 2.x format a
17
+ // previous version can still restore), `<id>.<stamp>.entries.json` (some
18
+ // entries), each with `.enc` appended when sealed. The stamp sorts lexically,
19
+ // so the name alone orders the copies of one id.
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.DEFAULT_QUARANTINE_DAYS = void 0;
25
+ exports.quarantineDays = quarantineDays;
26
+ exports.stampNow = stampNow;
27
+ exports.writeQuarantine = writeQuarantine;
28
+ exports.readQuarantine = readQuarantine;
29
+ exports.listQuarantine = listQuarantine;
30
+ exports.pruneQuarantine = pruneQuarantine;
31
+ exports.quarantineStats = quarantineStats;
32
+ exports.entriesAsNeuron = entriesAsNeuron;
33
+ exports.isEntriesCopy = isEntriesCopy;
34
+ const node_fs_1 = require("node:fs");
35
+ const node_path_1 = __importDefault(require("node:path"));
36
+ const atrest_js_1 = require("./atrest.js");
37
+ const fs_js_1 = require("../utils/fs.js");
38
+ exports.DEFAULT_QUARANTINE_DAYS = 180;
39
+ /** Retention in days from the environment; 0 means forever. Bad values fall back to the default. */
40
+ function quarantineDays(env = process.env) {
41
+ const raw = (env['CRBRO_QUARANTINE_DAYS'] || '').trim();
42
+ if (!raw)
43
+ return exports.DEFAULT_QUARANTINE_DAYS;
44
+ const n = Number(raw);
45
+ return Number.isInteger(n) && n >= 0 ? n : exports.DEFAULT_QUARANTINE_DAYS;
46
+ }
47
+ function stampNow() {
48
+ return (0, fs_js_1.now)().replace(/[:.]/g, '-');
49
+ }
50
+ /** "2026-11-13T10-20-30-123Z" → ISO, or null. */
51
+ function stampToIso(stamp) {
52
+ const m = stamp.match(/^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/);
53
+ return m ? `${m[1]}T${m[2]}:${m[3]}:${m[4]}.${m[5]}Z` : null;
54
+ }
55
+ function parseName(file) {
56
+ const sealed = file.endsWith('.json.enc');
57
+ if (!sealed && !file.endsWith('.json'))
58
+ return null;
59
+ const base = file.slice(0, file.length - (sealed ? '.json.enc'.length : '.json'.length));
60
+ const entries = base.endsWith('.entries');
61
+ const core = entries ? base.slice(0, -'.entries'.length) : base;
62
+ const m = core.match(/^(.*)\.(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)$/);
63
+ if (!m)
64
+ return { base, id: core, stamp: '', kind: entries ? 'entries' : 'whole', sealed };
65
+ return { base, id: m[1], stamp: m[2], kind: entries ? 'entries' : 'whole', sealed };
66
+ }
67
+ /** Write one copy; sealed when the switch is on. Throws rather than write in clear without a key. */
68
+ async function writeQuarantine(dir, id, data, kind = 'whole') {
69
+ await node_fs_1.promises.mkdir(dir, { recursive: true });
70
+ const { data: bytes, sealed } = (0, atrest_js_1.maybeSeal)(Buffer.from(JSON.stringify(data, null, 2), 'utf8'));
71
+ const name = `${id}.${stampNow()}${kind === 'entries' ? '.entries' : ''}.json${sealed ? '.enc' : ''}`;
72
+ const target = node_path_1.default.join(dir, name);
73
+ const tmp = `${target}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
74
+ await node_fs_1.promises.writeFile(tmp, bytes);
75
+ try {
76
+ await node_fs_1.promises.rename(tmp, target);
77
+ }
78
+ catch (err) {
79
+ await node_fs_1.promises.unlink(tmp).catch(() => { });
80
+ throw err;
81
+ }
82
+ return target;
83
+ }
84
+ async function readQuarantine(file) {
85
+ let raw;
86
+ try {
87
+ raw = await node_fs_1.promises.readFile(file);
88
+ }
89
+ catch (err) {
90
+ if (err.code === 'ENOENT')
91
+ return null;
92
+ throw err;
93
+ }
94
+ return JSON.parse((0, fs_js_1.stripBOM)((0, atrest_js_1.unseal)(raw).toString('utf8')));
95
+ }
96
+ /** Copies in the folder, optionally of one id, newest first. */
97
+ async function listQuarantine(dir, id) {
98
+ let names;
99
+ try {
100
+ names = await node_fs_1.promises.readdir(dir);
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ const out = [];
106
+ for (const file of names) {
107
+ if (file.endsWith('.tmp'))
108
+ continue;
109
+ const p = parseName(file);
110
+ if (!p)
111
+ continue;
112
+ if (id !== undefined && p.id !== id)
113
+ continue;
114
+ let taken = p.stamp ? stampToIso(p.stamp) : null;
115
+ if (!taken) {
116
+ try {
117
+ taken = (await node_fs_1.promises.stat(node_path_1.default.join(dir, file))).mtime.toISOString();
118
+ }
119
+ catch {
120
+ continue;
121
+ }
122
+ }
123
+ out.push({ file, base: p.base, id: p.id, kind: p.kind, sealed: p.sealed, taken });
124
+ }
125
+ return out.sort((a, b) => (a.taken < b.taken ? 1 : a.taken > b.taken ? -1 : a.file < b.file ? 1 : -1));
126
+ }
127
+ /**
128
+ * Delete copies older than `days` (0 = keep forever). Returns what went, or
129
+ * would go with dryRun. Only files this module names are ever touched.
130
+ */
131
+ async function pruneQuarantine(dir, opts = {}) {
132
+ const days = opts.days ?? quarantineDays();
133
+ const res = { pruned: 0, bytes: 0, files: [], days };
134
+ if (days <= 0)
135
+ return res;
136
+ const limite = (opts.now ?? new Date()).getTime() - days * 86_400_000;
137
+ for (const c of await listQuarantine(dir)) {
138
+ if (Date.parse(c.taken) >= limite)
139
+ continue;
140
+ const f = node_path_1.default.join(dir, c.file);
141
+ try {
142
+ const st = await node_fs_1.promises.stat(f);
143
+ if (!opts.dryRun)
144
+ await node_fs_1.promises.unlink(f);
145
+ res.pruned++;
146
+ res.bytes += st.size;
147
+ res.files.push(c.file);
148
+ }
149
+ catch { /* gone meanwhile */ }
150
+ }
151
+ return res;
152
+ }
153
+ /** Files, bytes and the oldest copy: what status and doctor show. */
154
+ async function quarantineStats(dir) {
155
+ const all = await listQuarantine(dir);
156
+ let bytes = 0;
157
+ for (const c of all) {
158
+ try {
159
+ bytes += (await node_fs_1.promises.stat(node_path_1.default.join(dir, c.file))).size;
160
+ }
161
+ catch { /* gone */ }
162
+ }
163
+ return {
164
+ files: all.length,
165
+ bytes,
166
+ oldest: all.length ? all[all.length - 1].taken : null,
167
+ sealed: all.filter(c => c.sealed).length,
168
+ };
169
+ }
170
+ /**
171
+ * The neuron-shaped part of a partial copy, ready for unionNeuron: the
172
+ * entries with their sidecars and the identity of the neuron they left.
173
+ */
174
+ function entriesAsNeuron(q) {
175
+ return {
176
+ id: q.neuron_id,
177
+ name: q.name,
178
+ type: q.type,
179
+ domain: q.domain,
180
+ created: q.taken_at,
181
+ last_accessed: q.taken_at,
182
+ access_count: 0,
183
+ heat: 0,
184
+ summary: '',
185
+ facts: q.facts || [],
186
+ decisions: q.decisions || [],
187
+ patterns: q.patterns || [],
188
+ preferences: q.preferences || [],
189
+ connections: [],
190
+ tags: [],
191
+ errors: q.errors || [],
192
+ debts: q.debts || [],
193
+ entry_dates: q.entry_dates || {},
194
+ ...(q.entry_status ? { entry_status: q.entry_status } : {}),
195
+ ...(q.entry_source ? { entry_source: q.entry_source } : {}),
196
+ ...(q.entry_verified ? { entry_verified: q.entry_verified } : {}),
197
+ ...(q.entry_verification ? { entry_verification: q.entry_verification } : {}),
198
+ ...(q.map ? { map: q.map } : {}),
199
+ };
200
+ }
201
+ function isEntriesCopy(x) {
202
+ return !!x && typeof x === 'object' && x.kind === 'entries' && x.quarantine_format === 2;
203
+ }
204
+ //# sourceMappingURL=quarantine.js.map
@@ -45,7 +45,15 @@ const PATTERNS = [
45
45
  kind: 'auth token (hex)',
46
46
  re: /\bauth[ _-]?token\b[^\S\n]{0,3}(?:[:=]|es|is)?[^\S\n]{0,3}["'`]?([0-9a-fA-F]{30,34})\b/gi,
47
47
  },
48
- { kind: 'private key block', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
48
+ // The whole block, not just its header: the body IS the key. Through the
49
+ // END line when there is one (16 KB is more than any real key); without
50
+ // it, every base64 token that follows (16+ characters, or a padded tail);
51
+ // the first ordinary word stops it. Matching only the header used to
52
+ // leave the body on disk while the benchmark counted it as caught.
53
+ {
54
+ kind: 'private key block',
55
+ re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----(?:[\s\S]{0,16384}?-----END [A-Z0-9 ]*PRIVATE KEY-----|(?:\s{1,4}(?:[A-Za-z0-9+/]{16,}={0,2}|[A-Za-z0-9+/]{2,}={1,2})(?!\S)){0,256})/g,
56
+ },
49
57
  { kind: 'JSON Web Token', re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
50
58
  // WordPress application password: six groups of four, anchored to the label.
51
59
  //
@@ -65,8 +73,14 @@ const PATTERNS = [
65
73
  // copula ("la contraseña es X", "the password is X") with no separator,
66
74
  // requiring the value to mix classes so plain words do not match.
67
75
  {
76
+ // Three shapes after the separator are prose, not a value, and are left
77
+ // alone (seen on a real brain): a capitalised product name ("Reset
78
+ // contraseña = Supabase Auth built-in"), an assignment ("contraseña:
79
+ // key_filename=~/.ssh/id_x") and a path ("~/.ssh/…", "/etc/…").
68
80
  kind: 'password',
69
- re: /\b(?:password|passwd|contrase[nñ]a)\s*(?:es|is)?\s*[:=]\s*["'`]?([^\s"'`,;]{8,})/gi,
81
+ // No /i flag: the capitalised-word check needs case. The label is
82
+ // spelled in the three casings people write.
83
+ re: /\b(?:[Pp]assword|PASSWORD|[Pp]asswd|PASSWD|[Cc]ontrase[nñ]a|CONTRASE[NÑ]A)\s*(?:es|is|ES|IS)?\s*[:=]\s*["'`]?(?![A-Z][a-z]+(?![^\s"'`,;]))(?![A-Za-z_]+=)(?!~?\/)([^\s"'`,;]{8,})/g,
70
84
  },
71
85
  {
72
86
  // "clave" is also the Spanish adjective "key/important": "Decisión clave:
@@ -89,6 +103,26 @@ const PATTERNS = [
89
103
  kind: 'password (prose)',
90
104
  re: /\b(?:password|passwd|contrase[nñ]a|clave)\b(?:\s+(?:de|del|of|for)\s+[^\s]{1,30})?\s+(?:es|is)\s+["'`]?((?=[^\s"'`,;]*[A-Za-z])(?=[^\s"'`,;]*[0-9])[^\s"'`,;]{8,})/gi,
91
105
  },
106
+ {
107
+ // The label with no separator and no copula, as people really jot it:
108
+ // "PASS Xy7#kk22w", "la contraseña Xy7kk22W (la del panel)". Without a
109
+ // separator the value carries the whole burden of proof, so it must mix
110
+ // three of lower case, upper case, digit and symbol — the rule that keeps
111
+ // "clave" the adjective out (2.9.1). Case-sensitive labels for the same
112
+ // reason. "pass" in lower case is an English verb ("pass userId42 to…"),
113
+ // so it gets its own rule below; "clave" stays out of this one entirely.
114
+ kind: 'password',
115
+ re: /\b(?:PASS|PASSWORD|PASSWD|[Pp]assword|[Pp]asswd|[Cc]ontrase[nñ]a|CONTRASE[NÑ]A)\b[^\S\n]+["'`]?((?:(?=[^\s"'`,;()]*[a-z])(?=[^\s"'`,;()]*[A-Z])(?=[^\s"'`,;()]*[0-9])|(?=[^\s"'`,;()]*[a-z])(?=[^\s"'`,;()]*[A-Z])(?=[^\s"'`,;()]*[!@#$%^&*+=?~])|(?=[^\s"'`,;()]*[a-z])(?=[^\s"'`,;()]*[0-9])(?=[^\s"'`,;()]*[!@#$%^&*+=?~])|(?=[^\s"'`,;()]*[A-Z])(?=[^\s"'`,;()]*[0-9])(?=[^\s"'`,;()]*[!@#$%^&*+=?~]))[^\s"'`,;()]{8,})/g,
116
+ },
117
+ {
118
+ // "pass" as a label: "user admin pass: Xy7kk22w", "pass Xy7#kk22w".
119
+ // With ":"/"=" the value needs a letter and a digit or symbol ("pass:
120
+ // completed" is a test report). Without a separator it needs a letter, a
121
+ // digit AND a symbol, and may not start with $ or - — that is what keeps
122
+ // "pass userId42 to the helper", "pass user_id2" and "pass $TOKEN2" out.
123
+ kind: 'password',
124
+ re: /\b(?:PASS|[Pp]ass)\b(?:[^\S\n]*[:=][^\S\n]*["'`]?((?=[^\s"'`,;()]*[A-Za-z])(?=[^\s"'`,;()]*[0-9!@#$%^&*+=?~])[^\s"'`,;()]{8,})|[^\S\n]+["'`]?(?![$-])((?=[^\s"'`,;()]*[A-Za-z])(?=[^\s"'`,;()]*[0-9])(?=[^\s"'`,;()]*[!@#$%^&*+=?~])[^\s"'`,;()]{8,}))/g,
125
+ },
92
126
  {
93
127
  // A credential dictated in two pieces to slip past filters: "the token
94
128
  // starts with ghp_ and continues with <body>". Needs a credential noun,
@@ -133,9 +167,11 @@ function findSecrets(text) {
133
167
  re.lastIndex = 0;
134
168
  let m;
135
169
  while ((m = re.exec(text)) !== null) {
136
- // For the labelled-password pattern, redact only the value, not the label.
137
- const value = m[1] !== undefined ? m[1] : m[0];
138
- const at = m[1] !== undefined ? m.index + m[0].indexOf(m[1]) : m.index;
170
+ // For the labelled patterns, redact only the value, not the label. A
171
+ // pattern with alternatives captures the value in whichever group matched.
172
+ const group = m.slice(1).find(g => g !== undefined);
173
+ const value = group !== undefined ? group : m[0];
174
+ const at = group !== undefined ? m.index + m[0].lastIndexOf(group) : m.index;
139
175
  hits.push({ kind, index: at, length: value.length });
140
176
  if (m[0].length === 0)
141
177
  re.lastIndex++;
@@ -103,19 +103,39 @@ const VERSIONED = [
103
103
  ];
104
104
  const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
105
105
  const VERSIONED_RE = new RegExp(`(?:^|[^a-z0-9.])(?:${VERSIONED.map(escape).join('|')})[\\s-]?v?\\d+(?:\\.\\d+)*(?![\\d.]*\\d)`, 'i');
106
+ /**
107
+ * X.Y.Z as a version, but not a thousands-separated number: in Spanish
108
+ * "1.200.000 visitas" is a count, and it made the fact volatile (2.11).
109
+ * Every group after the first exactly three digits = a number, not a version.
110
+ */
111
+ function semverLike(t) {
112
+ const re = /(?:^|[^\d.])(\d{1,3}(?:\.\d{1,3}){2,})(?:-[0-9a-z.]+)?(?![\d.]*\d)/g;
113
+ for (const m of t.matchAll(re)) {
114
+ const grupos = m[1].split('.');
115
+ if (grupos.length !== 3)
116
+ continue;
117
+ if (grupos.slice(1).every(g => g.length === 3))
118
+ continue; // 1.200.000
119
+ return true;
120
+ }
121
+ return false;
122
+ }
106
123
  const RULES = [
107
124
  {
108
125
  reason: 'version',
109
126
  test: (_raw, t) => /\bv\d+(?:\.\d+)+\b/.test(t) // v2.3
110
127
  || /\b(?:version|release)\s*:?\s*v?\d+(?:\.\d+)*/.test(t) // version 3.6, versión 5
111
- || /(?:^|[^\d.])\d{1,3}\.\d{1,3}\.\d{1,3}(?:-[0-9a-z.]+)?(?![\d.]*\d)/.test(t) // 2.8.0, 1.4.2-beta (not a dd.mm.yyyy day)
128
+ || semverLike(t) // 2.8.0, 1.4.2-beta (not a dd.mm.yyyy day, not 1.200.000)
112
129
  || VERSIONED_RE.test(t), // PostgreSQL 14, Node 22
113
130
  },
114
131
  {
115
132
  reason: 'price',
116
133
  test: (_raw, t) => /[€$£¥]\s?\d|\d(?:[.,]\d+)?\s?[€$£¥]/.test(t) // $49, 29 €
117
134
  || /\b\d+(?:[.,]\d+)?\s*(?:euros?|eur|usd|dollars?|dolares|libras|pounds?|gbp|mxn|pesos)\b/.test(t)
118
- || /\d[^.\n]{0,40}(?:\bal mes\b|\bpor mes\b|\/mes\b|\bper month\b|\ba month\b|\/month\b|\/mo\b|\bal ano\b|\bpor ano\b|\bper year\b|\/year\b|\/yr\b|\bmensuales?\b|\banuales?\b)/.test(t)
135
+ // A number right before the period ("29 al mes", "29/mes", "9.99 per month"). Not
136
+ // "3 informes mensuales" or "4 informes al mes" (2.11): a count of things is no
137
+ // price, and with money in the sentence the currency rules above already fire.
138
+ || /\d(?:[.,]\d+)?\s?(?:k\s?)?(?:al mes\b|por mes\b|\/mes\b|per month\b|a month\b|\/month\b|\/mo\b|al ano\b|por ano\b|per year\b|\/year\b|\/yr\b)/.test(t)
119
139
  || /\b(?:price|pricing|precio|precios|tarifa|tarifas|cuesta|cuestan|cost|costs|fee|cuota)\b[^.\n]{0,30}\d/.test(t),
120
140
  },
121
141
  {
@@ -145,7 +165,9 @@ const RULES = [
145
165
  test: (raw, t) => /\b[A-Z][A-Z0-9]*_[A-Z0-9_]+\s*=\s*\S+/.test(raw) // KEY_NAME=value
146
166
  || /(?:^|\s)--[a-z][\w-]*=\S+/.test(raw) // --max-old-space-size=4096
147
167
  || /\b[a-z][a-z0-9]*_[a-z0-9_]+\b\s*(?:=|:|de|a|en|to|at|is|es)?\s*\d/.test(t) // memory_limit de 512M
148
- || /\b(?:set to|configured (?:to|as|at)|configurad[oa]s? (?:a|en|como)|fijad[oa]s? (?:a|en)|establecid[oa]s? (?:a|en)|timeout|time-out|ttl|rate limit|limit|limite|max|maximo|maximum|minimo|minimum|retries|reintentos|threshold|umbral|batch size|pool size|flag)\b[^.\n]{0,25}?\d/.test(t),
168
+ || /\b(?:set to|configured (?:to|as|at)|configurad[oa]s? (?:a|en|como)|fijad[oa]s? (?:a|en)|establecid[oa]s? (?:a|en)|timeout|time-out|ttl|rate limit|limit|limite|maximo|maximum|minimo|minimum|retries|reintentos|threshold|umbral|batch size|pool size|flag)\b[^.\n]{0,25}?\d/.test(t)
169
+ // "max" only as written in lower case: "Max Planck nacio en 1858" is a name (2.11).
170
+ || /\b(?:max|MAX)\b[^.\n]{0,25}?\d/.test(raw),
149
171
  },
150
172
  ];
151
173
  /** Role words, as written (first letter either case, rest lower) or as acronyms. */
@@ -452,11 +474,11 @@ function recordVerdict(text) {
452
474
  * record of something done (2.9.1), else volatile when a rule fires (and
453
475
  * which one), normal otherwise.
454
476
  */
455
- function detectShelf(text) {
477
+ function detectShelf(text, temporal) {
456
478
  const raw = String(text || '');
457
479
  if (!raw.trim())
458
480
  return { shelf: 'normal' };
459
- if (isDatedRecord(raw))
481
+ if (temporal !== 'current' && isDatedRecord(raw))
460
482
  return { shelf: 'permanent', reason: 'history' };
461
483
  const t = fold(raw);
462
484
  for (const r of RULES) {
@@ -474,17 +496,18 @@ function detectShelf(text) {
474
496
  * made about the present — warning on it is noise); else the one its text
475
497
  * implies.
476
498
  *
477
- * Once a session has said the same line too, it is no longer only an
478
- * imported note: the duplicate branch of learn keeps `source: "miner"` but
479
- * sets `verified` or adds a confirmation, and from then on the line is judged
480
- * by its text like any other claim about the present.
499
+ * `current` impide inferir historia por origen o texto, sin verificar nada.
500
+ * Sin temporalidad explícita se conserva la clasificación legacy por
501
+ * confirmations; repetir ya no cambia la fecha de verificación.
481
502
  */
482
503
  function shelfOfFact(f) {
483
504
  if (isShelfLife(f.shelf_life))
484
505
  return { shelf: f.shelf_life, inferred: false };
485
- if (f.source === 'miner' && !f.verified && (f.confirmations ?? 1) <= 1)
506
+ if (f.temporal === 'historical')
507
+ return { shelf: 'permanent', inferred: true, reason: 'history' };
508
+ if (f.temporal !== 'current' && f.source === 'miner' && !f.verified && (f.confirmations ?? 1) <= 1)
486
509
  return { shelf: 'permanent', inferred: true, reason: 'miner' };
487
- const d = detectShelf(f.text);
510
+ const d = detectShelf(f.text, f.temporal);
488
511
  return { shelf: d.shelf, inferred: true, ...(d.reason ? { reason: d.reason } : {}) };
489
512
  }
490
513
  /**