fraim 2.0.263 → 2.0.264
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.
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.CATEGORY_TO_FILETYPE = exports.BRAIN_LEARNING_FILETYPE_TO_REGION = exports.LEARNING_PRIORITIES = void 0;
|
|
10
10
|
exports.learningEntryHeadingRegex = learningEntryHeadingRegex;
|
|
11
|
+
exports.getScoreThreshold = getScoreThreshold;
|
|
11
12
|
exports.computeEffectiveScore = computeEffectiveScore;
|
|
12
13
|
exports.mergeLearningEntriesAcrossTiers = mergeLearningEntriesAcrossTiers;
|
|
13
14
|
exports.buildLearningContextSection = buildLearningContextSection;
|
|
@@ -1195,9 +1196,14 @@ function listDomainLearningFiles(dir, prefix, fileType) {
|
|
|
1195
1196
|
return out;
|
|
1196
1197
|
}
|
|
1197
1198
|
// Parse the `## [P-…] Title` entries (and their body prose) out of one learning
|
|
1198
|
-
// file. Score
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1199
|
+
// file. Score metadata (Last seen / Recurrences) is captured to compute the
|
|
1200
|
+
// effective decayed score and the injected flag; those bookkeeping lines are
|
|
1201
|
+
// still excluded from the rendered body.
|
|
1202
|
+
//
|
|
1203
|
+
// threshold: entries with score >= threshold (or in a non-gated category) are
|
|
1204
|
+
// marked injected:true. Callers that don't need scoring pass Infinity so
|
|
1205
|
+
// all entries are injected.
|
|
1206
|
+
function parseLearningEntries(filePath, displayPath, category, level, domain = 'global', tier, threshold = DEFAULT_THRESHOLD) {
|
|
1201
1207
|
if (!(0, fs_1.existsSync)(filePath))
|
|
1202
1208
|
return [];
|
|
1203
1209
|
let content;
|
|
@@ -1207,23 +1213,52 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
|
|
|
1207
1213
|
catch {
|
|
1208
1214
|
return [];
|
|
1209
1215
|
}
|
|
1216
|
+
const fileType = exports.CATEGORY_TO_FILETYPE[category];
|
|
1217
|
+
// preferences and manager-coaching are threshold-free — always injected.
|
|
1218
|
+
const isScoreGated = fileType === 'mistake-patterns' || fileType === 'validated-patterns';
|
|
1210
1219
|
const out = [];
|
|
1211
1220
|
let current = null;
|
|
1212
1221
|
let bodyLines = [];
|
|
1222
|
+
// Scoring metadata for the current entry.
|
|
1223
|
+
let curLastSeen = null;
|
|
1224
|
+
let curRecurrences = 1;
|
|
1213
1225
|
const flush = () => {
|
|
1214
1226
|
if (current) {
|
|
1215
1227
|
current.body = bodyLines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
1228
|
+
if (isScoreGated) {
|
|
1229
|
+
if (curLastSeen !== null) {
|
|
1230
|
+
const s = computeEffectiveScore(current.severity, curLastSeen, curRecurrences, fileType);
|
|
1231
|
+
current.score = s;
|
|
1232
|
+
current.injected = s >= threshold;
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
current.score = null;
|
|
1236
|
+
// No date metadata: treat as fresh / always inject.
|
|
1237
|
+
current.injected = true;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
current.score = null;
|
|
1242
|
+
current.injected = true;
|
|
1243
|
+
}
|
|
1216
1244
|
out.push(current);
|
|
1217
1245
|
}
|
|
1218
1246
|
current = null;
|
|
1219
1247
|
bodyLines = [];
|
|
1248
|
+
curLastSeen = null;
|
|
1249
|
+
curRecurrences = 1;
|
|
1220
1250
|
};
|
|
1221
1251
|
const headingRe = learningEntryHeadingRegex();
|
|
1222
1252
|
for (const line of content.split(/\r?\n/)) {
|
|
1223
1253
|
const header = line.match(headingRe);
|
|
1224
1254
|
if (header) {
|
|
1225
1255
|
flush();
|
|
1226
|
-
const entry = {
|
|
1256
|
+
const entry = {
|
|
1257
|
+
severity: header[1],
|
|
1258
|
+
title: header[2].trim(),
|
|
1259
|
+
body: '', source: displayPath, category, level, domain,
|
|
1260
|
+
score: null, injected: true,
|
|
1261
|
+
};
|
|
1227
1262
|
if (tier !== undefined)
|
|
1228
1263
|
entry.tier = tier;
|
|
1229
1264
|
current = entry;
|
|
@@ -1235,10 +1270,19 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
|
|
|
1235
1270
|
flush();
|
|
1236
1271
|
continue;
|
|
1237
1272
|
}
|
|
1238
|
-
// Drop the bookkeeping lines — the Hub shows the human-readable learning,
|
|
1239
|
-
// not the scoring metadata.
|
|
1240
1273
|
const t = line.trim();
|
|
1241
|
-
|
|
1274
|
+
// Capture scoring metadata but exclude from body.
|
|
1275
|
+
const lastSeenMatch = t.match(/^\*\*Last seen\*\*:\s*(.+)/i);
|
|
1276
|
+
if (lastSeenMatch) {
|
|
1277
|
+
curLastSeen = lastSeenMatch[1].trim();
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
const recurrencesMatch = t.match(/^\*\*Recurrences\*\*:\s*(\d+)/i);
|
|
1281
|
+
if (recurrencesMatch) {
|
|
1282
|
+
curRecurrences = parseInt(recurrencesMatch[1], 10) || 1;
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if (/^\*\*(Score|Technical trace|Users|First synthesized)\*\*:/i.test(t))
|
|
1242
1286
|
continue;
|
|
1243
1287
|
if (/^(First|Last) synthesized:/i.test(t))
|
|
1244
1288
|
continue;
|
|
@@ -1247,44 +1291,85 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
|
|
|
1247
1291
|
flush();
|
|
1248
1292
|
return out;
|
|
1249
1293
|
}
|
|
1250
|
-
function levelDir(roots, level) {
|
|
1251
|
-
if (level === 'machine') {
|
|
1252
|
-
// Issue #1070: use the configured manager home, not the portable authoring base.
|
|
1253
|
-
// For single-machine backends managerHomeBase == globalPersonalBase (no change).
|
|
1254
|
-
return { dir: roots.managerHomeBase, displayBase: roots.managerHomeDisplayBase.replace(/\/$/, '') };
|
|
1255
|
-
}
|
|
1256
|
-
return { dir: roots.repoLearningsBase, displayBase: REPO_LEARNINGS_REL };
|
|
1257
|
-
}
|
|
1258
1294
|
function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine') {
|
|
1259
1295
|
const roots = getLearningRoots(workspaceRoot);
|
|
1296
|
+
const threshold = getScoreThreshold(workspaceRoot);
|
|
1260
1297
|
const out = [];
|
|
1298
|
+
// Helper: parse + merge a list of tier paths for one file family.
|
|
1299
|
+
// ResolvedLearningTier uses 'manager' to mean machine-level personal; map it to 'machine'.
|
|
1300
|
+
const tierLevel = (t) => t.level === 'manager' ? 'machine' : t.level;
|
|
1301
|
+
const parseTiers = (tiers, cat, tierVal, domainVal) => mergeLearningEntriesAcrossTiers(tiers.map((t) => ({
|
|
1302
|
+
entries: parseLearningEntries(t.path, t.displayPath, cat, tierLevel(t), domainVal ?? 'global', tierVal, threshold),
|
|
1303
|
+
})));
|
|
1261
1304
|
if (scope === 'org') {
|
|
1262
1305
|
for (const cat of ['avoid', 'preference', 'repeat', 'coaching']) {
|
|
1263
1306
|
const fileType = exports.CATEGORY_TO_FILETYPE[cat];
|
|
1264
1307
|
const f = `org-${fileType}.md`;
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1308
|
+
// org pack home first, repo-local override second; repo-local wins on title collision.
|
|
1309
|
+
out.push(...parseTiers(resolveOrgLearningFileTiers(roots.repoLearningsBase, f), cat));
|
|
1310
|
+
// #806: domain-scoped org learnings — union from both locations.
|
|
1311
|
+
const allDomainFiles = [
|
|
1312
|
+
...listDomainLearningFiles(roots.repoLearningsBase, 'org', fileType).map((d) => ({ ...d, base: roots.repoLearningsBase })),
|
|
1313
|
+
...listDomainLearningFiles((0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, 'learnings'), 'org', fileType)
|
|
1314
|
+
.map((d) => ({ ...d, base: (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, 'learnings') })),
|
|
1315
|
+
];
|
|
1316
|
+
const seenDomainFile = new Set();
|
|
1317
|
+
for (const { fileName, domain, base } of allDomainFiles) {
|
|
1318
|
+
if (seenDomainFile.has(fileName))
|
|
1319
|
+
continue;
|
|
1320
|
+
seenDomainFile.add(fileName);
|
|
1321
|
+
const domainTiers = resolveOrgLearningFileTiers(roots.repoLearningsBase, fileName);
|
|
1322
|
+
if (domainTiers.length === 0) {
|
|
1323
|
+
out.push(...parseLearningEntries((0, path_1.join)(base, fileName), `${REPO_LEARNINGS_REL}/${fileName}`, cat, 'org', domain, undefined, threshold));
|
|
1324
|
+
}
|
|
1325
|
+
else {
|
|
1326
|
+
out.push(...parseTiers(domainTiers, cat, undefined, domain));
|
|
1327
|
+
}
|
|
1269
1328
|
}
|
|
1270
1329
|
}
|
|
1271
1330
|
return out;
|
|
1272
1331
|
}
|
|
1332
|
+
// manager + reverse: use the same three-tier resolution as buildLearningContextSection.
|
|
1273
1333
|
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
1274
|
-
const { dir, displayBase } = levelDir(roots, level);
|
|
1275
1334
|
const cats = scope === 'reverse' ? ['coaching'] : ['avoid', 'preference', 'repeat'];
|
|
1335
|
+
if (level === 'project') {
|
|
1336
|
+
// Project level: repo-local only (single tier, no merge needed).
|
|
1337
|
+
for (const cat of cats) {
|
|
1338
|
+
const fileType = exports.CATEGORY_TO_FILETYPE[cat];
|
|
1339
|
+
const f = `${resolvedUserId}-${fileType}.md`;
|
|
1340
|
+
out.push(...parseLearningEntries((0, path_1.join)(roots.repoLearningsBase, f), `${REPO_LEARNINGS_REL}/${f}`, cat, 'project', 'global', 'hot', threshold));
|
|
1341
|
+
const coldF = `${resolvedUserId}-${fileType}${COLD_FILE_SUFFIX}.md`;
|
|
1342
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, coldF))) {
|
|
1343
|
+
out.push(...parseLearningEntries((0, path_1.join)(roots.repoLearningsBase, coldF), `${REPO_LEARNINGS_REL}/${coldF}`, cat, 'project', 'global', 'cold', threshold));
|
|
1344
|
+
}
|
|
1345
|
+
for (const { fileName, domain } of listDomainLearningFiles(roots.repoLearningsBase, resolvedUserId, fileType)) {
|
|
1346
|
+
out.push(...parseLearningEntries((0, path_1.join)(roots.repoLearningsBase, fileName), `${REPO_LEARNINGS_REL}/${fileName}`, cat, 'project', domain, 'hot', threshold));
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return out;
|
|
1350
|
+
}
|
|
1351
|
+
// machine level: all three personal tiers (managerCacheBase, globalPersonalBase, repoLearningsBase).
|
|
1352
|
+
// Cold siblings are read from each tier that has the hot file.
|
|
1276
1353
|
for (const cat of cats) {
|
|
1277
1354
|
const fileType = exports.CATEGORY_TO_FILETYPE[cat];
|
|
1278
1355
|
const f = `${resolvedUserId}-${fileType}.md`;
|
|
1279
|
-
|
|
1280
|
-
|
|
1356
|
+
const tiers = resolvePersonalLearningFileTiers(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, f);
|
|
1357
|
+
out.push(...parseTiers(tiers, cat, 'hot'));
|
|
1358
|
+
// Cold sibling from each tier that has the hot file.
|
|
1281
1359
|
const coldF = `${resolvedUserId}-${fileType}${COLD_FILE_SUFFIX}.md`;
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1360
|
+
const coldTiers = resolvePersonalLearningFileTiers(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, coldF);
|
|
1361
|
+
out.push(...parseTiers(coldTiers, cat, 'cold'));
|
|
1362
|
+
// #806: domain files from managerHomeBase and repo base.
|
|
1363
|
+
const domainDirs = [roots.globalPersonalBase, roots.managerCacheBase, roots.repoLearningsBase];
|
|
1364
|
+
const seenDomain = new Set();
|
|
1365
|
+
for (const dir of domainDirs) {
|
|
1366
|
+
for (const { fileName, domain } of listDomainLearningFiles(dir, resolvedUserId, fileType)) {
|
|
1367
|
+
if (seenDomain.has(fileName))
|
|
1368
|
+
continue;
|
|
1369
|
+
seenDomain.add(fileName);
|
|
1370
|
+
const domTiers = resolvePersonalLearningFileTiers(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, fileName);
|
|
1371
|
+
out.push(...parseTiers(domTiers, cat, 'hot', domain));
|
|
1372
|
+
}
|
|
1288
1373
|
}
|
|
1289
1374
|
}
|
|
1290
1375
|
return out;
|
|
@@ -1292,8 +1377,12 @@ function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine')
|
|
|
1292
1377
|
function resolveLearningFilePath(workspaceRoot, userId, ref) {
|
|
1293
1378
|
const roots = getLearningRoots(workspaceRoot);
|
|
1294
1379
|
const fileType = exports.CATEGORY_TO_FILETYPE[ref.category];
|
|
1295
|
-
if (ref.scope === 'org')
|
|
1296
|
-
|
|
1380
|
+
if (ref.scope === 'org') {
|
|
1381
|
+
// Write org entries to the org pack home — same location share-with-others uses.
|
|
1382
|
+
// The Hub read path already merges pack home + repo-local, so edits land where
|
|
1383
|
+
// the agent reads them without needing a repo commit.
|
|
1384
|
+
return (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, 'learnings', `org-${fileType}.md`);
|
|
1385
|
+
}
|
|
1297
1386
|
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
1298
1387
|
// Issue #1070: for the 'machine' level use the configured manager home.
|
|
1299
1388
|
const base = ref.level === 'project' ? roots.repoLearningsBase : roots.managerHomeBase;
|