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.
- package/LICENSE +21 -21
- package/README.md +256 -465
- package/SECURITY.md +114 -12
- package/bin/crbro.mjs +539 -300
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +139 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +667 -232
- package/dist/engine/doctor.js +312 -14
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +18 -7
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +721 -162
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +29 -6
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +25 -3
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +797 -176
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +583 -373
- package/dist/sync/git.js +203 -72
- package/dist/sync/space.js +100 -25
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +94 -7
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +9 -5
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -682
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/doctor.d.ts +0 -93
- package/dist/engine/doctor.d.ts.map +0 -1
- package/dist/engine/doctor.js.map +0 -1
- package/dist/engine/evidence.d.ts +0 -26
- package/dist/engine/evidence.d.ts.map +0 -1
- package/dist/engine/evidence.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -64
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/projects.d.ts +0 -20
- package/dist/engine/projects.d.ts.map +0 -1
- package/dist/engine/projects.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -155
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -328
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/quality.d.ts +0 -18
- package/dist/search/quality.d.ts.map +0 -1
- package/dist/search/quality.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -36
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -147
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -118
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -369
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -13
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── CRBRO Shared Memory: what comes from outside ────────────────
|
|
3
|
+
//
|
|
4
|
+
// Space names, remotes and branches come from a tool call (so possibly from
|
|
5
|
+
// text the model was shown); neuron directories and ops come from teammates'
|
|
6
|
+
// pushes. Each of them used to be joined into a path, passed to git or written
|
|
7
|
+
// into a neuron as it came. These checks are the one place that says what
|
|
8
|
+
// shapes are accepted (G1, G5, G16).
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.NEURON_DIR_RE = exports.SPACE_NAME_RE = void 0;
|
|
14
|
+
exports.spaceNameProblem = spaceNameProblem;
|
|
15
|
+
exports.safeSpaceDir = safeSpaceDir;
|
|
16
|
+
exports.remoteProblem = remoteProblem;
|
|
17
|
+
exports.branchProblem = branchProblem;
|
|
18
|
+
exports.cleanAuthor = cleanAuthor;
|
|
19
|
+
exports.sanitizeIncomingOps = sanitizeIncomingOps;
|
|
20
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
21
|
+
const secrets_js_1 = require("../engine/secrets.js");
|
|
22
|
+
const WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
23
|
+
/** A space name is a folder under shared/: a short slug, nothing that walks. */
|
|
24
|
+
exports.SPACE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
25
|
+
function spaceNameProblem(name) {
|
|
26
|
+
if (typeof name !== 'string' || !exports.SPACE_NAME_RE.test(name)) {
|
|
27
|
+
return 'Space names are 1-64 letters, digits, "-" or "_", starting with a letter or digit (e.g. "equipo").';
|
|
28
|
+
}
|
|
29
|
+
if (WINDOWS_RESERVED.test(name))
|
|
30
|
+
return `"${name}" is a reserved device name on Windows.`;
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/** Throws unless `name` is a valid space name whose folder stays inside `sharedRoot`. */
|
|
34
|
+
function safeSpaceDir(sharedRoot, name) {
|
|
35
|
+
const problem = spaceNameProblem(name);
|
|
36
|
+
if (problem)
|
|
37
|
+
throw new Error(`Invalid space name. ${problem}`);
|
|
38
|
+
const root = node_path_1.default.resolve(sharedRoot);
|
|
39
|
+
const dir = node_path_1.default.resolve(root, name);
|
|
40
|
+
const rel = node_path_1.default.relative(root, dir);
|
|
41
|
+
if (!rel || rel.startsWith('..') || node_path_1.default.isAbsolute(rel) || rel.includes(node_path_1.default.sep) || rel.includes('/')) {
|
|
42
|
+
throw new Error('Invalid space name: it must name a folder directly inside shared/.');
|
|
43
|
+
}
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A remote: a git URL or a local path. Never an option ("-…"), never with a
|
|
48
|
+
* password or token inside (it would be stored in .git/config in clear and
|
|
49
|
+
* echoed back in messages), never with control characters.
|
|
50
|
+
*/
|
|
51
|
+
function remoteProblem(remote) {
|
|
52
|
+
if (typeof remote !== 'string' || !remote.trim())
|
|
53
|
+
return 'A remote is required.';
|
|
54
|
+
if (remote.length > 2048)
|
|
55
|
+
return 'That remote is too long.';
|
|
56
|
+
if (remote.startsWith('-'))
|
|
57
|
+
return 'A remote cannot start with "-".';
|
|
58
|
+
if (/[\u0000-\u001f\u007f]/.test(remote))
|
|
59
|
+
return 'A remote cannot contain control characters.';
|
|
60
|
+
const url = /^([a-z][a-z0-9+.-]*):\/\/([^/?#]*)/i.exec(remote);
|
|
61
|
+
if (url) {
|
|
62
|
+
const scheme = url[1].toLowerCase();
|
|
63
|
+
const authority = url[2];
|
|
64
|
+
const at = authority.lastIndexOf('@');
|
|
65
|
+
if (at !== -1) {
|
|
66
|
+
const userinfo = authority.slice(0, at);
|
|
67
|
+
if (scheme === 'http' || scheme === 'https' || userinfo.includes(':')) {
|
|
68
|
+
return 'The remote carries credentials (user:password@ or token@). Remove them: use an SSH URL or a credential helper. ' +
|
|
69
|
+
'A URL with a credential would be written in clear to .git/config.';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if ((0, secrets_js_1.secretKinds)(remote).length > 0)
|
|
74
|
+
return 'The remote looks like it contains a credential. Remove it from the URL.';
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
/** git check-ref-format rules, the subset a branch name here needs. */
|
|
78
|
+
function branchProblem(branch) {
|
|
79
|
+
if (typeof branch !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,99}$/.test(branch) ||
|
|
80
|
+
branch.includes('..') || branch.includes('//') || branch.endsWith('/') || branch.endsWith('.') ||
|
|
81
|
+
branch.endsWith('.lock') || branch.includes('@{') || /\/\./.test(branch)) {
|
|
82
|
+
return 'Branch names are letters, digits, ".", "_", "-" or "/", starting with a letter or digit (e.g. "main").';
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/** A neuron folder name in a space: what CRBRO itself would have written. */
|
|
87
|
+
exports.NEURON_DIR_RE = /^[a-z0-9][a-z0-9_-]{0,159}$/;
|
|
88
|
+
const KNOWN_OPS = new Set(['neuron', 'fact', 'status', 'decision', 'pattern', 'tag', 'error', 'map', 'purge', 'debt', 'verify']);
|
|
89
|
+
const NEURON_TYPES = new Set(['project', 'tech', 'lang', 'person', 'domain', 'process', 'protocol']);
|
|
90
|
+
const MAX_TEXT = 20_000;
|
|
91
|
+
const MAX_ID = 128;
|
|
92
|
+
function cleanText(v, max = MAX_TEXT) {
|
|
93
|
+
if (typeof v !== 'string')
|
|
94
|
+
return null;
|
|
95
|
+
return (0, secrets_js_1.redact)(v.length > max ? v.slice(0, max) : v).text;
|
|
96
|
+
}
|
|
97
|
+
function cleanId(v) {
|
|
98
|
+
return typeof v === 'string' && v.length > 0 && v.length <= MAX_ID ? v : null;
|
|
99
|
+
}
|
|
100
|
+
/** Same charset as getIdentity, 32 characters: `by` ends up in `origin` labels. */
|
|
101
|
+
function cleanAuthor(v) {
|
|
102
|
+
const s = typeof v === 'string' ? v.toLowerCase().replace(/[^a-z0-9_-]/g, '').slice(0, 32) : '';
|
|
103
|
+
return s || 'unknown';
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Ops read from a teammate's log for the folder `nid`, made safe to merge:
|
|
107
|
+
* an op that names another neuron is dropped (the folder decides, so a planted
|
|
108
|
+
* `nid` cannot steer where a neuron is written); text is capped and passed
|
|
109
|
+
* through the secret filter; authors, ids, confidences and kinds are checked.
|
|
110
|
+
* Returns the ops kept and how many were dropped.
|
|
111
|
+
*/
|
|
112
|
+
function sanitizeIncomingOps(nid, ops) {
|
|
113
|
+
const out = [];
|
|
114
|
+
let dropped = 0;
|
|
115
|
+
for (const raw of ops) {
|
|
116
|
+
const op = raw;
|
|
117
|
+
if (!op || typeof op !== 'object' || op['nid'] !== nid || typeof op['op'] !== 'string' || !KNOWN_OPS.has(op['op'])) {
|
|
118
|
+
dropped++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const at = typeof op['at'] === 'string' && op['at'].length <= 40 ? op['at'] : '';
|
|
122
|
+
const base = { ...op, nid, by: cleanAuthor(op['by']), at };
|
|
123
|
+
let kept = null;
|
|
124
|
+
switch (op['op']) {
|
|
125
|
+
case 'neuron': {
|
|
126
|
+
const name = cleanText(op['name'], 200);
|
|
127
|
+
kept = {
|
|
128
|
+
...base,
|
|
129
|
+
name: name ?? nid,
|
|
130
|
+
ntype: typeof op['ntype'] === 'string' && NEURON_TYPES.has(op['ntype']) ? op['ntype'] : 'project',
|
|
131
|
+
domain: typeof op['domain'] === 'string' ? op['domain'].toLowerCase().replace(/[^a-z0-9_-]/g, '').slice(0, 60) || 'general' : 'general',
|
|
132
|
+
};
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case 'fact': {
|
|
136
|
+
const text = cleanText(op['text']);
|
|
137
|
+
const fid = cleanId(op['fid']);
|
|
138
|
+
if (text === null || !fid)
|
|
139
|
+
break;
|
|
140
|
+
const conf = typeof op['conf'] === 'number' && Number.isFinite(op['conf']) ? Math.min(1, Math.max(0, op['conf'])) : 1;
|
|
141
|
+
const keys = Array.isArray(op['keys'])
|
|
142
|
+
? op['keys'].filter((k) => typeof k === 'string').slice(0, 8).map(k => (0, secrets_js_1.redact)(k.slice(0, 120)).text)
|
|
143
|
+
: undefined;
|
|
144
|
+
kept = { ...base, text, fid, conf, ...(keys ? { keys } : {}),
|
|
145
|
+
...(typeof op['src'] === 'string' ? { src: op['src'].slice(0, 60) } : {}) };
|
|
146
|
+
if (!keys)
|
|
147
|
+
delete kept['keys'];
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case 'status': {
|
|
151
|
+
const fid = cleanId(op['fid']);
|
|
152
|
+
if (!fid || (op['to'] !== 'superseded' && op['to'] !== 'retracted'))
|
|
153
|
+
break;
|
|
154
|
+
const why = op['why'] === undefined ? undefined : cleanText(op['why'], 2_000) ?? undefined;
|
|
155
|
+
kept = { ...base, fid, why };
|
|
156
|
+
if (why === undefined)
|
|
157
|
+
delete kept['why'];
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'decision': {
|
|
161
|
+
const text = cleanText(op['text']);
|
|
162
|
+
const did = cleanId(op['did']);
|
|
163
|
+
if (text === null || !did)
|
|
164
|
+
break;
|
|
165
|
+
const why = op['why'] === undefined ? undefined : cleanText(op['why'], 4_000) ?? undefined;
|
|
166
|
+
kept = { ...base, text, did, why };
|
|
167
|
+
if (why === undefined)
|
|
168
|
+
delete kept['why'];
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case 'pattern':
|
|
172
|
+
case 'tag':
|
|
173
|
+
case 'error':
|
|
174
|
+
case 'debt':
|
|
175
|
+
case 'map': {
|
|
176
|
+
const text = cleanText(op['text'], op['op'] === 'tag' ? 200 : MAX_TEXT);
|
|
177
|
+
if (text === null)
|
|
178
|
+
break;
|
|
179
|
+
kept = { ...base, text };
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case 'purge': {
|
|
183
|
+
const key = cleanId(op['key']);
|
|
184
|
+
if (!key)
|
|
185
|
+
break;
|
|
186
|
+
kept = { ...base, key };
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case 'verify': {
|
|
190
|
+
const eid = cleanId(op['eid']);
|
|
191
|
+
if (!eid || (op['ekind'] !== 'fact' && op['ekind'] !== 'entry'))
|
|
192
|
+
break;
|
|
193
|
+
kept = { ...base, eid };
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (kept)
|
|
198
|
+
out.push(kept);
|
|
199
|
+
else
|
|
200
|
+
dropped++;
|
|
201
|
+
}
|
|
202
|
+
return { ops: out, dropped };
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=validate.js.map
|
package/dist/utils/budget.js
CHANGED
|
@@ -87,13 +87,20 @@ function fitToBudget(payload, opts = {}) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
};
|
|
90
|
+
// The block a path belongs to: "open_items[3].text" → "open_items".
|
|
91
|
+
const topKey = (p) => p.split(/[.[]/)[0];
|
|
92
|
+
const order = opts.dropFirst ?? [];
|
|
93
|
+
const rank = (p) => {
|
|
94
|
+
const i = order.indexOf(topKey(p));
|
|
95
|
+
return i === -1 ? order.length : i;
|
|
96
|
+
};
|
|
90
97
|
let guard = 0;
|
|
91
98
|
while (size(copy) > budget && guard++ < 200) {
|
|
92
99
|
const found = [];
|
|
93
100
|
arraysOf(copy, '', found);
|
|
94
101
|
const biggest = found
|
|
95
102
|
.filter(a => a.arr.length > 1)
|
|
96
|
-
.sort((a, b) => size(b.arr) - size(a.arr))[0];
|
|
103
|
+
.sort((a, b) => rank(a.path) - rank(b.path) || size(b.arr) - size(a.arr))[0];
|
|
97
104
|
if (!biggest)
|
|
98
105
|
break;
|
|
99
106
|
const total = dropped[biggest.path]?.total ?? biggest.arr.length;
|
|
@@ -116,7 +123,13 @@ function fitToBudget(payload, opts = {}) {
|
|
|
116
123
|
}
|
|
117
124
|
if (Object.keys(dropped).length)
|
|
118
125
|
report.entries_dropped = dropped;
|
|
119
|
-
if (
|
|
126
|
+
if (report.returned_chars > budget)
|
|
127
|
+
report.over_budget_after_cuts = true;
|
|
128
|
+
const cutKeys = [...new Set([...Object.keys(shortened), ...Object.keys(dropped)].map(topKey))];
|
|
129
|
+
const doors = opts.moreByKey ? cutKeys.map(k => opts.moreByKey[k]).filter(Boolean) : [];
|
|
130
|
+
if (doors.length)
|
|
131
|
+
report.how_to_get_more = [...new Set(doors)].join(' ');
|
|
132
|
+
else if (opts.howToGetMore)
|
|
120
133
|
report.how_to_get_more = opts.howToGetMore;
|
|
121
134
|
copy.truncated = report;
|
|
122
135
|
return copy;
|
package/dist/utils/fs.js
CHANGED
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
6
|
};
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.CorruptJSONError = void 0;
|
|
9
|
+
exports.stripBOM = stripBOM;
|
|
8
10
|
exports.readJSON = readJSON;
|
|
9
11
|
exports.writeJSON = writeJSON;
|
|
10
12
|
exports.fileExists = fileExists;
|
|
@@ -19,20 +21,52 @@ exports.now = now;
|
|
|
19
21
|
const node_fs_1 = require("node:fs");
|
|
20
22
|
const node_path_1 = __importDefault(require("node:path"));
|
|
21
23
|
const lock_js_1 = require("./lock.js");
|
|
24
|
+
/**
|
|
25
|
+
* A file that exists and cannot be parsed. Its own class so a reader that
|
|
26
|
+
* walks the whole cortex can skip it, say which file and move on, instead of
|
|
27
|
+
* letting one bad neuron take recall, maintenance and consolidate down with it.
|
|
28
|
+
* The message keeps the 2.x wording ("Corrupted JSON at <path>").
|
|
29
|
+
*/
|
|
30
|
+
class CorruptJSONError extends Error {
|
|
31
|
+
path;
|
|
32
|
+
reason;
|
|
33
|
+
constructor(path, reason) {
|
|
34
|
+
super(`CRBRO: Corrupted JSON at ${path}: ${reason}`);
|
|
35
|
+
this.path = path;
|
|
36
|
+
this.reason = reason;
|
|
37
|
+
this.name = 'CorruptJSONError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.CorruptJSONError = CorruptJSONError;
|
|
41
|
+
/**
|
|
42
|
+
* Text of a JSON file without a leading byte-order mark. Windows PowerShell
|
|
43
|
+
* 5.1 (`Set-Content -Encoding utf8`, `Out-File`) writes one, and JSON.parse
|
|
44
|
+
* rejects it: a neuron edited by hand that way became "corrupted" for every
|
|
45
|
+
* reader although every byte of its content was fine.
|
|
46
|
+
*/
|
|
47
|
+
function stripBOM(text) {
|
|
48
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
49
|
+
}
|
|
22
50
|
/**
|
|
23
51
|
* Safely read and parse a JSON file. Returns null if file doesn't exist.
|
|
24
|
-
* Never throws on missing file — only on corrupted JSON.
|
|
52
|
+
* Never throws on missing file — only on corrupted JSON (CorruptJSONError).
|
|
25
53
|
*/
|
|
26
54
|
async function readJSON(filePath) {
|
|
55
|
+
let content;
|
|
27
56
|
try {
|
|
28
|
-
|
|
29
|
-
return JSON.parse(content);
|
|
57
|
+
content = await node_fs_1.promises.readFile(filePath, 'utf-8');
|
|
30
58
|
}
|
|
31
59
|
catch (err) {
|
|
32
60
|
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
33
61
|
return null;
|
|
34
62
|
}
|
|
35
|
-
throw new
|
|
63
|
+
throw new CorruptJSONError(filePath, err instanceof Error ? err.message : String(err));
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(stripBOM(content));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
throw new CorruptJSONError(filePath, err instanceof Error ? err.message : String(err));
|
|
36
70
|
}
|
|
37
71
|
}
|
|
38
72
|
/**
|
|
@@ -88,13 +122,13 @@ async function fileExists(filePath) {
|
|
|
88
122
|
*
|
|
89
123
|
* The mutator returns the value to write, or null to leave the file alone.
|
|
90
124
|
*/
|
|
91
|
-
async function updateJSON(filePath, mutate) {
|
|
125
|
+
async function updateJSON(filePath, mutate, options) {
|
|
92
126
|
return (0, lock_js_1.withLock)(filePath, async () => {
|
|
93
127
|
const current = await readJSON(filePath);
|
|
94
128
|
const next = await mutate(current);
|
|
95
129
|
if (next === null)
|
|
96
130
|
return current;
|
|
97
|
-
await writeJSON(filePath, next);
|
|
131
|
+
await writeJSON(filePath, next, options);
|
|
98
132
|
return next;
|
|
99
133
|
});
|
|
100
134
|
}
|
package/dist/utils/ids.js
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
// Generate and validate neuron/synapse IDs
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.toSnakeCase = toSnakeCase;
|
|
6
|
+
exports.slugCandidates = slugCandidates;
|
|
6
7
|
exports.legacySnakeCase = legacySnakeCase;
|
|
7
8
|
exports.neuronId = neuronId;
|
|
9
|
+
exports.typeOfIdShape = typeOfIdShape;
|
|
8
10
|
exports.synapseId = synapseId;
|
|
9
11
|
exports.sessionId = sessionId;
|
|
10
12
|
exports.isValidNeuronId = isValidNeuronId;
|
|
11
13
|
exports.techKeywordIn = techKeywordIn;
|
|
12
14
|
exports.inferNeuronType = inferNeuronType;
|
|
15
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
16
|
const TYPE_PREFIXES = {
|
|
14
17
|
project: 'project_',
|
|
15
18
|
tech: 'tech_',
|
|
@@ -20,10 +23,52 @@ const TYPE_PREFIXES = {
|
|
|
20
23
|
protocol: 'protocol_',
|
|
21
24
|
};
|
|
22
25
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
26
|
+
* Names whose punctuation IS the identity. Without this, "C", "C++" and "C#"
|
|
27
|
+
* all slugged to "c" and shared one neuron. Applied before anything else, as
|
|
28
|
+
* whole words only, so "c++ tips" maps and "abc++" does not.
|
|
25
29
|
*/
|
|
26
|
-
|
|
30
|
+
const SYMBOL_WORDS = [
|
|
31
|
+
[/(^|[^\p{L}\p{N}])c\+\+(?![\p{L}\p{N}])/giu, '$1cpp'],
|
|
32
|
+
[/(^|[^\p{L}\p{N}])c#(?![\p{L}\p{N}])/giu, '$1csharp'],
|
|
33
|
+
[/(^|[^\p{L}\p{N}])f#(?![\p{L}\p{N}])/giu, '$1fsharp'],
|
|
34
|
+
];
|
|
35
|
+
function mapSymbolWords(str) {
|
|
36
|
+
let out = str;
|
|
37
|
+
for (const [re, to] of SYMBOL_WORDS)
|
|
38
|
+
out = out.replace(re, to);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** Latin letters NFD does not decompose: they used to vanish ("Straße" → "strae"). */
|
|
42
|
+
const LATIN_EXTRA = {
|
|
43
|
+
'ß': 'ss', 'ẞ': 'ss', 'ø': 'o', 'Ø': 'o', 'ł': 'l', 'Ł': 'l', 'đ': 'd', 'Đ': 'd', 'ð': 'd', 'Ð': 'd',
|
|
44
|
+
'þ': 'th', 'Þ': 'th', 'æ': 'ae', 'Æ': 'ae', 'œ': 'oe', 'Œ': 'oe', 'ı': 'i', 'ħ': 'h', 'Ħ': 'h',
|
|
45
|
+
};
|
|
46
|
+
/** Trivial one-to-one transliteration: Greek and Cyrillic. Anything else is hashed. */
|
|
47
|
+
const TRANSLIT = (() => {
|
|
48
|
+
const t = {};
|
|
49
|
+
const cyr = 'а:a б:b в:v г:g д:d е:e ё:e ж:zh з:z и:i й:y к:k л:l м:m н:n о:o п:p р:r с:s т:t у:u ф:f х:kh ц:ts ч:ch ш:sh щ:shch ъ: ы:y ь: э:e ю:yu я:ya і:i ї:yi є:ye ґ:g ў:u';
|
|
50
|
+
const gre = 'α:a β:v γ:g δ:d ε:e ζ:z η:i θ:th ι:i κ:k λ:l μ:m ν:n ξ:x ο:o π:p ρ:r σ:s ς:s τ:t υ:y φ:f χ:ch ψ:ps ω:o';
|
|
51
|
+
for (const pair of `${cyr} ${gre}`.split(' ')) {
|
|
52
|
+
const [k, v] = pair.split(':');
|
|
53
|
+
t[k] = v;
|
|
54
|
+
}
|
|
55
|
+
return t;
|
|
56
|
+
})();
|
|
57
|
+
function transliterate(str) {
|
|
58
|
+
let out = '';
|
|
59
|
+
for (const ch of str) {
|
|
60
|
+
const lower = ch.toLowerCase();
|
|
61
|
+
if (LATIN_EXTRA[ch] !== undefined)
|
|
62
|
+
out += LATIN_EXTRA[ch];
|
|
63
|
+
else if (TRANSLIT[lower] !== undefined)
|
|
64
|
+
out += TRANSLIT[lower];
|
|
65
|
+
else
|
|
66
|
+
out += ch;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/** The 2.10 algorithm, kept as a lookup fallback: neurons created by it live under its slugs. */
|
|
71
|
+
function slug210(str) {
|
|
27
72
|
return str
|
|
28
73
|
.normalize('NFD') // Split accents off their letters...
|
|
29
74
|
.replace(/[\u0300-\u036f]/g, '') // ...and drop the accents, keeping the letter
|
|
@@ -34,6 +79,55 @@ function toSnakeCase(str) {
|
|
|
34
79
|
.replace(/_+/g, '_') // Collapse multiple underscores
|
|
35
80
|
.replace(/^_|_$/g, ''); // Trim leading/trailing underscores
|
|
36
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Convert a string to a valid snake_case ID.
|
|
84
|
+
* "OctoChat Plugin" → "octochat_plugin"
|
|
85
|
+
*
|
|
86
|
+
* Never empty for a name that has any character in it: a name whose letters
|
|
87
|
+
* are not Latin (and not trivially transliterable) gets a short stable hash of
|
|
88
|
+
* the whole name, so "東京" and "大阪" are two neurons, not one called
|
|
89
|
+
* "project_" that every such topic fell into.
|
|
90
|
+
*/
|
|
91
|
+
function toSnakeCase(str) {
|
|
92
|
+
return slugOf(str).slug;
|
|
93
|
+
}
|
|
94
|
+
function slugOf(str) {
|
|
95
|
+
const raw = (str || '').trim();
|
|
96
|
+
if (!raw)
|
|
97
|
+
return { slug: '', hashed: false };
|
|
98
|
+
const folded = mapSymbolWords(raw).normalize('NFD').replace(/[\u0300-\u036f]/g, '').normalize('NFC');
|
|
99
|
+
const prepared = transliterate(folded);
|
|
100
|
+
const base = slug210(prepared);
|
|
101
|
+
// Letters or digits that are still not a-z/0-9 were dropped by slug210:
|
|
102
|
+
// their information must not be lost, or two names differing only there
|
|
103
|
+
// would collide.
|
|
104
|
+
const lost = /[^\x00-\x7f]/.test(prepared.replace(/[^\p{L}\p{N}]/gu, ''));
|
|
105
|
+
if (base && !lost)
|
|
106
|
+
return { slug: base, hashed: false };
|
|
107
|
+
const hash = (0, node_crypto_1.createHash)('sha1').update(raw.normalize('NFC').toLowerCase(), 'utf8').digest('hex').slice(0, 8);
|
|
108
|
+
return { slug: base ? `${base}_u${hash}` : `u${hash}`, hashed: true };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Every slug an existing neuron for this name may be filed under, newest rule
|
|
112
|
+
* first: the current one, the 2.10 one, and the pre-1.x accent-mangling one.
|
|
113
|
+
* The older two are computed on the symbol-mapped name, so "C++" never falls
|
|
114
|
+
* back to the "c" that 2.10 gave it — that fallback is exactly the merge the
|
|
115
|
+
* new rule exists to stop. Nor does a name the new rule had to hash: 2.10
|
|
116
|
+
* dropped its letters, so its old slug is shared by unrelated names.
|
|
117
|
+
*/
|
|
118
|
+
function slugCandidates(name) {
|
|
119
|
+
const out = [];
|
|
120
|
+
const add = (s) => { if (s && !out.includes(s))
|
|
121
|
+
out.push(s); };
|
|
122
|
+
const actual = slugOf(name);
|
|
123
|
+
add(actual.slug);
|
|
124
|
+
if (actual.hashed)
|
|
125
|
+
return out;
|
|
126
|
+
const mapped = mapSymbolWords((name || '').trim());
|
|
127
|
+
add(slug210(mapped));
|
|
128
|
+
add(legacySnakeCase(mapped));
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
37
131
|
/**
|
|
38
132
|
* How ids were built before accents were folded: the accented letter was
|
|
39
133
|
* deleted whole, so "búsqueda" became "bsqueda" and "técnico" became "tcnico".
|
|
@@ -57,9 +151,21 @@ function legacySnakeCase(str) {
|
|
|
57
151
|
*/
|
|
58
152
|
function neuronId(name, type) {
|
|
59
153
|
const prefix = TYPE_PREFIXES[type];
|
|
60
|
-
|
|
154
|
+
let slug = toSnakeCase(name);
|
|
155
|
+
// A name that already is an id of this type ("project_x") keeps its one
|
|
156
|
+
// prefix: it used to become "project_project_x".
|
|
157
|
+
if (slug.startsWith(prefix) && slug.length > prefix.length)
|
|
158
|
+
slug = slug.slice(prefix.length);
|
|
61
159
|
return `${prefix}${slug}`;
|
|
62
160
|
}
|
|
161
|
+
/** The type an id-shaped slug names ("tech_react" → tech), or null. */
|
|
162
|
+
function typeOfIdShape(slug) {
|
|
163
|
+
for (const [type, prefix] of Object.entries(TYPE_PREFIXES)) {
|
|
164
|
+
if (slug.startsWith(prefix) && slug.length > prefix.length)
|
|
165
|
+
return type;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
63
169
|
/**
|
|
64
170
|
* Generate a synapse ID from two neuron IDs.
|
|
65
171
|
* ("project_octochat", "tech_firebase") → "syn_octochat__firebase"
|
|
@@ -99,27 +205,35 @@ function techKeywordIn(text) {
|
|
|
99
205
|
/**
|
|
100
206
|
* Infer neuron type from topic name using simple heuristics.
|
|
101
207
|
* Falls back to 'project' if unsure.
|
|
208
|
+
*
|
|
209
|
+
* Whole words only (accents folded). Substrings sent "Juego Onoris Lanista"
|
|
210
|
+
* to lang_ ("go "), "PAGO UNICO" too, and anything "digital" to tech_ ("git").
|
|
211
|
+
* The type is the id prefix, so a wrong guess is a wrong identity.
|
|
102
212
|
*/
|
|
103
213
|
function inferNeuronType(topic) {
|
|
104
|
-
const
|
|
214
|
+
const folded = (topic || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim();
|
|
215
|
+
const words = new Set(folded.split(/[^a-z0-9]+/).filter(Boolean));
|
|
216
|
+
const has = (kw) => words.has(kw);
|
|
105
217
|
// Tech keywords
|
|
106
|
-
if (TECH_KEYWORDS.some(
|
|
218
|
+
if (TECH_KEYWORDS.some(has))
|
|
107
219
|
return 'tech';
|
|
108
220
|
// Language keywords
|
|
109
|
-
const langKeywords = ['python', 'javascript', 'typescript', 'php', 'rust', 'go', 'java', 'swift', 'kotlin', 'css', 'html', 'sql'];
|
|
110
|
-
if (langKeywords.some(
|
|
221
|
+
const langKeywords = ['python', 'javascript', 'typescript', 'php', 'rust', 'go', 'golang', 'java', 'swift', 'kotlin', 'css', 'html', 'sql'];
|
|
222
|
+
if (langKeywords.some(has))
|
|
111
223
|
return 'lang';
|
|
112
224
|
// Process keywords
|
|
113
|
-
const processKeywords = ['deploy', 'migration', 'workflow', 'pipeline', '
|
|
114
|
-
if (processKeywords.some(
|
|
225
|
+
const processKeywords = ['deploy', 'migration', 'workflow', 'pipeline', 'backup', 'setup', 'install'];
|
|
226
|
+
if (processKeywords.some(has) || /(^|[^a-z0-9])ci\/cd([^a-z0-9]|$)/.test(folded))
|
|
115
227
|
return 'process';
|
|
116
228
|
// Domain keywords
|
|
117
229
|
const domainKeywords = ['seo', 'marketing', 'trading', 'crypto', 'design', 'security', 'devops', 'data', 'analytics'];
|
|
118
|
-
if (domainKeywords.some(kw =>
|
|
230
|
+
if (domainKeywords.some(kw => folded === kw))
|
|
119
231
|
return 'domain';
|
|
120
|
-
// Protocol keywords
|
|
121
|
-
|
|
122
|
-
|
|
232
|
+
// Protocol keywords. "protocolo" keeps working: a Spanish protocol neuron
|
|
233
|
+
// is injected at boot, and the substring rule always caught it.
|
|
234
|
+
const protocolKeywords = ['protocol', 'protocols', 'protocolo', 'protocolos', 'directive', 'directives', 'directiva', 'directivas',
|
|
235
|
+
'rule', 'rules', 'enforcement', 'behavioral'];
|
|
236
|
+
if (protocolKeywords.some(has))
|
|
123
237
|
return 'protocol';
|
|
124
238
|
// Default to project
|
|
125
239
|
return 'project';
|