fraim 2.0.346 → 2.0.349
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/dist/registry/scripts/learning/completion-state.js +80 -4
- package/dist/registry/scripts/learning/corpus-audit.js +7 -7
- package/dist/registry/scripts/learning/entry-authoring.js +181 -0
- package/dist/registry/scripts/learning/entry-format.js +10 -1
- package/dist/registry/scripts/learning/outcome-assessment.js +162 -0
- package/dist/registry/scripts/learning/outcome-types.js +92 -0
- package/dist/registry/scripts/learning/owner-identity.js +11 -0
- package/dist/registry/scripts/learning/proposal-queue.js +81 -0
- package/dist/registry/scripts/learning/run-application.js +1003 -0
- package/dist/registry/scripts/learning/run-publication.js +364 -0
- package/dist/registry/scripts/learning/run-record.js +895 -0
- package/dist/registry/scripts/learning-usage/learning-source-record.js +131 -0
- package/dist/src/cli/commands/learning.js +64 -0
- package/dist/src/cli/doctor/checks/scripts-checks.js +53 -1
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/learning/inventory.js +285 -0
- package/dist/src/cli/learning/queue-operations.js +199 -0
- package/dist/src/cli/learning/runtime.js +82 -0
- package/dist/src/cli/learning/usage.js +277 -0
- package/dist/src/core/fraim-config-schema.generated.js +0 -4
- package/dist/src/core/utils/local-registry-resolver.js +13 -3
- package/dist/src/local-mcp-server/learning-context-builder.js +109 -379
- package/dist/src/local-mcp-server/learning-entry-format.js +25 -0
- package/dist/src/local-mcp-server/learning-usage-analysis.js +12 -410
- package/dist/src/local-mcp-server/learning-usage-eligibility.js +8 -14
- package/dist/src/local-mcp-server/learning-usage-projection.js +5 -22
- package/dist/src/local-mcp-server/learning-usage-store.js +93 -393
- package/dist/src/local-mcp-server/learning-usage-verify.js +9 -40
- package/dist/src/local-mcp-server/stdio-server.js +50 -114
- package/dist/src/mcp/tool-schemas.js +2 -1
- package/package.json +9 -1
- package/dist/src/local-mcp-server/learning-firing-parser.js +0 -247
- package/dist/src/local-mcp-server/learning-usage-attestation.js +0 -238
- package/dist/src/local-mcp-server/learning-usage-command.js +0 -518
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CATEGORY_TO_FILETYPE = exports.BRAIN_LEARNING_FILETYPE_TO_REGION = exports.TEAM_CONTEXT_RELATIVE_PATHS = exports.LEARNING_ENTRY_ID_REGEX = exports.LEARNING_PRIORITIES = exports.DEFAULT_THRESHOLD = void 0;
|
|
4
|
-
exports.learningEntryHeadingRegex = learningEntryHeadingRegex;
|
|
5
|
-
exports.learningEntryIdLineRegex = learningEntryIdLineRegex;
|
|
6
|
-
exports.generateLearningEntryId = generateLearningEntryId;
|
|
3
|
+
exports.CATEGORY_TO_FILETYPE = exports.BRAIN_LEARNING_FILETYPE_TO_REGION = exports.TEAM_CONTEXT_RELATIVE_PATHS = exports.generateLearningEntryId = exports.learningEntryIdLineRegex = exports.learningEntryHeadingRegex = exports.LEARNING_ENTRY_ID_REGEX = exports.LEARNING_PRIORITIES = exports.DEFAULT_THRESHOLD = void 0;
|
|
7
4
|
exports.getScoreThreshold = getScoreThreshold;
|
|
8
5
|
exports.computeEffectiveScore = computeEffectiveScore;
|
|
9
6
|
exports.getLearningInputHomes = getLearningInputHomes;
|
|
10
7
|
exports.readLearningSourceFile = readLearningSourceFile;
|
|
11
8
|
exports.mergeLearningEntriesAcrossTiers = mergeLearningEntriesAcrossTiers;
|
|
12
9
|
exports.buildLearningContextSection = buildLearningContextSection;
|
|
13
|
-
exports.collectOfferedLearningEntries = collectOfferedLearningEntries;
|
|
14
|
-
exports.collectOfferedRuleFiles = collectOfferedRuleFiles;
|
|
15
10
|
exports.buildTeamContextSection = buildTeamContextSection;
|
|
16
11
|
exports.resolveTeamContextFiles = resolveTeamContextFiles;
|
|
17
12
|
exports.isTeamContextKey = isTeamContextKey;
|
|
@@ -33,10 +28,11 @@ const artifact_paths_1 = require("../../registry/scripts/artifact-paths");
|
|
|
33
28
|
* workspace root on the user's machine.
|
|
34
29
|
*/
|
|
35
30
|
const fs_1 = require("fs");
|
|
36
|
-
const crypto_1 = require("crypto");
|
|
37
31
|
const path_1 = require("path");
|
|
38
32
|
const learning_source_record_1 = require("./learning-source-record");
|
|
39
33
|
const completion_state_1 = require("../../registry/scripts/learning/completion-state");
|
|
34
|
+
const outcome_types_1 = require("../../registry/scripts/learning/outcome-types");
|
|
35
|
+
const owner_identity_1 = require("../../registry/scripts/learning/owner-identity");
|
|
40
36
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
41
37
|
const brand_store_1 = require("../core/brand-store");
|
|
42
38
|
// Issue #1043 round 2: org and manager content lives at one home named by
|
|
@@ -44,10 +40,6 @@ const brand_store_1 = require("../core/brand-store");
|
|
|
44
40
|
// cache, which silently returned nothing once that cache stopped existing.
|
|
45
41
|
const pack_home_1 = require("../cli/utils/pack-home");
|
|
46
42
|
const learning_domains_1 = require("../config/learning-domains");
|
|
47
|
-
// Key construction only. The store's read/write path is still required lazily
|
|
48
|
-
// below, because that side pulls in the retention config and a static cycle
|
|
49
|
-
// through this module would be fragile.
|
|
50
|
-
const learning_usage_store_1 = require("./learning-usage-store");
|
|
51
43
|
const REPO_LEARNINGS_REL = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/learnings').replace(/\/$/, '');
|
|
52
44
|
/**
|
|
53
45
|
* The score at or above which an entry is injected. Exported so a test asserts
|
|
@@ -70,41 +62,13 @@ const L0_SLEEP_ON_LEARNINGS_PROMPT_MIN = 5;
|
|
|
70
62
|
// from LEARNING_PRIORITIES, and a build-time contract check
|
|
71
63
|
// (scripts/validate-learning-format-contract.ts, run by `npm run build`) asserts
|
|
72
64
|
// all three sides agree. Issue #1103 added the `**Id**` line to the same contract.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
*
|
|
81
|
-
* A firing record has to survive a later title edit, which a title-derived key
|
|
82
|
-
* cannot do — so the identity is persisted on the entry rather than computed at
|
|
83
|
-
* read time. Generation is deterministic from (family, original title) only so a
|
|
84
|
-
* re-run of the migration never renumbers the corpus; once written, the id never
|
|
85
|
-
* changes, including when the title does.
|
|
86
|
-
*/
|
|
87
|
-
/** What `generateLearningEntryId` emits: `L-` plus exactly ten hex characters. */
|
|
88
|
-
exports.LEARNING_ENTRY_ID_REGEX = /^L-[0-9a-f]{10}$/;
|
|
89
|
-
/**
|
|
90
|
-
* What the reader accepts. Deliberately wider than what the generator emits: a
|
|
91
|
-
* hand-written or hand-edited id must be honoured rather than silently ignored,
|
|
92
|
-
* because ignoring it would leave the entry looking un-stamped and route its
|
|
93
|
-
* firings to a title key without saying so.
|
|
94
|
-
*/
|
|
95
|
-
const LEARNING_ENTRY_ID_LINE = /^\*\*Id\*\*:\s*(L-[0-9A-Za-z]{6,24})\s*$/;
|
|
96
|
-
function learningEntryIdLineRegex() {
|
|
97
|
-
return new RegExp(LEARNING_ENTRY_ID_LINE.source);
|
|
98
|
-
}
|
|
99
|
-
function generateLearningEntryId(fileType, title) {
|
|
100
|
-
// Ten hex characters: short enough for an agent to type into a retrospective
|
|
101
|
-
// by hand, wide enough that a collision across one manager's corpus is not a
|
|
102
|
-
// practical concern.
|
|
103
|
-
const digest = (0, crypto_1.createHash)('sha1')
|
|
104
|
-
.update(`${fileType}|${title.trim().replace(/\s+/g, ' ').toLowerCase()}`)
|
|
105
|
-
.digest('hex');
|
|
106
|
-
return `L-${digest.slice(0, 10)}`;
|
|
107
|
-
}
|
|
65
|
+
const learning_entry_format_1 = require("./learning-entry-format");
|
|
66
|
+
var learning_entry_format_2 = require("./learning-entry-format");
|
|
67
|
+
Object.defineProperty(exports, "LEARNING_PRIORITIES", { enumerable: true, get: function () { return learning_entry_format_2.LEARNING_PRIORITIES; } });
|
|
68
|
+
Object.defineProperty(exports, "LEARNING_ENTRY_ID_REGEX", { enumerable: true, get: function () { return learning_entry_format_2.LEARNING_ENTRY_ID_REGEX; } });
|
|
69
|
+
Object.defineProperty(exports, "learningEntryHeadingRegex", { enumerable: true, get: function () { return learning_entry_format_2.learningEntryHeadingRegex; } });
|
|
70
|
+
Object.defineProperty(exports, "learningEntryIdLineRegex", { enumerable: true, get: function () { return learning_entry_format_2.learningEntryIdLineRegex; } });
|
|
71
|
+
Object.defineProperty(exports, "generateLearningEntryId", { enumerable: true, get: function () { return learning_entry_format_2.generateLearningEntryId; } });
|
|
108
72
|
/**
|
|
109
73
|
* The manager location that is NOT the portable authoring base.
|
|
110
74
|
*
|
|
@@ -348,7 +312,7 @@ function getScoreThreshold(workspaceRoot) {
|
|
|
348
312
|
* @param now Optional override for "now" (for forward-looking aging-risk
|
|
349
313
|
* calculations). Defaults to the current wall clock.
|
|
350
314
|
*/
|
|
351
|
-
function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType, now = new Date()
|
|
315
|
+
function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType, now = new Date()) {
|
|
352
316
|
const baseScore = severity === 'P-CRITICAL' ? 10 : severity === 'P-HIGH' ? 8 : severity === 'P-MED' ? 5 : 3;
|
|
353
317
|
// Mistake patterns decay faster (90d) — they're tied to environments that change.
|
|
354
318
|
// Preferences, manager-coaching, and validated-patterns express durable judgment (180d half-life).
|
|
@@ -361,55 +325,12 @@ function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType, no
|
|
|
361
325
|
catch {
|
|
362
326
|
// Treat as fresh.
|
|
363
327
|
}
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
// mistake happened again — so an entry that fires and prevents its mistake
|
|
367
|
-
// generates no signal and decays *because* it worked. Reading the newer of the
|
|
368
|
-
// two dates fixes that inversion with one changed input: no new term, the same
|
|
369
|
-
// half-lives, the same threshold. The newer date is the smaller elapsed time,
|
|
370
|
-
// hence min() here. An absent or unparseable firing date contributes nothing,
|
|
371
|
-
// so it can never silently resurrect a dormant entry, and an entry with no
|
|
372
|
-
// usage record at all scores exactly as it did before.
|
|
373
|
-
let daysSinceClock = daysSinceLastSeen;
|
|
374
|
-
const lastFiredMs = parseClockDate(lastFiredDate);
|
|
375
|
-
if (lastFiredMs > 0 && !isImplausiblyFuture(lastFiredMs, now)) {
|
|
376
|
-
const daysSinceFired = Math.max(0, (now.getTime() - lastFiredMs) / (1000 * 60 * 60 * 24));
|
|
377
|
-
daysSinceClock = Number.isNaN(daysSinceClock) ? daysSinceFired : Math.min(daysSinceClock, daysSinceFired);
|
|
378
|
-
}
|
|
379
|
-
const decay = Math.pow(0.5, daysSinceClock / halfLife);
|
|
328
|
+
// Selection uses recorded learning experience, independently of optional usage history.
|
|
329
|
+
const decay = Math.pow(0.5, daysSinceLastSeen / halfLife);
|
|
380
330
|
const recurrenceBoost = Math.log2(Math.max(1, recurrences) + 1);
|
|
381
331
|
return baseScore * decay * recurrenceBoost;
|
|
382
332
|
}
|
|
383
|
-
|
|
384
|
-
const FUTURE_FIRING_TOLERANCE_MS = 24 * 60 * 60 * 1000;
|
|
385
|
-
/**
|
|
386
|
-
* Is this firing date far enough ahead of now to be untrustworthy?
|
|
387
|
-
*
|
|
388
|
-
* `lastFiredAt` is stamped by whichever machine ran the job, and the usage store
|
|
389
|
-
* lives in the manager home, which is commonly a folder synced across machines.
|
|
390
|
-
* A machine running fast therefore writes a date another machine reads as the
|
|
391
|
-
* future, and elapsed time clamps to zero, so decay stops and the entry stays at
|
|
392
|
-
* full score indefinitely. That is the opposite of a decay model.
|
|
393
|
-
*
|
|
394
|
-
* A day of tolerance absorbs ordinary skew, where treating the firing as "just
|
|
395
|
-
* now" is right anyway. Beyond that the date is not believable, so it is ignored
|
|
396
|
-
* exactly as an unparseable one is, leaving the recurrence date in charge.
|
|
397
|
-
*/
|
|
398
|
-
function isImplausiblyFuture(timestampMs, now) {
|
|
399
|
-
return timestampMs - now.getTime() > FUTURE_FIRING_TOLERANCE_MS;
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Parse one side of the decay clock. An absent or unparseable date contributes
|
|
403
|
-
* nothing, so a malformed `last fired` can never silently resurrect a dormant
|
|
404
|
-
* entry: it just leaves the recurrence date in charge.
|
|
405
|
-
*/
|
|
406
|
-
function parseClockDate(value) {
|
|
407
|
-
if (!value)
|
|
408
|
-
return 0;
|
|
409
|
-
const parsed = new Date(value).getTime();
|
|
410
|
-
return Number.isNaN(parsed) ? 0 : parsed;
|
|
411
|
-
}
|
|
412
|
-
function scanMistakePatternFile(filePath, threshold, fileType = 'mistake-patterns', usageClock) {
|
|
333
|
+
function scanMistakePatternFile(filePath, threshold, fileType = 'mistake-patterns') {
|
|
413
334
|
const empty = { active: 0, dormant: 0, agingRisk: 0 };
|
|
414
335
|
if (!(0, fs_1.existsSync)(filePath))
|
|
415
336
|
return empty;
|
|
@@ -429,20 +350,17 @@ function scanMistakePatternFile(filePath, threshold, fileType = 'mistake-pattern
|
|
|
429
350
|
let scanned = 0;
|
|
430
351
|
let inEntry = false;
|
|
431
352
|
let severity = null;
|
|
432
|
-
let title = '';
|
|
433
|
-
let entryId = null;
|
|
434
353
|
let lastSeen = '';
|
|
435
354
|
let recurrences = 1;
|
|
436
355
|
const flush = () => {
|
|
437
356
|
if (!severity)
|
|
438
357
|
return;
|
|
439
358
|
scanned++;
|
|
440
|
-
const
|
|
441
|
-
const today = computeEffectiveScore(severity, lastSeen, recurrences, fileType, now, lastFired);
|
|
359
|
+
const today = computeEffectiveScore(severity, lastSeen, recurrences, fileType, now);
|
|
442
360
|
if (today >= threshold) {
|
|
443
361
|
active++;
|
|
444
362
|
if (lastSeen) {
|
|
445
|
-
const future = computeEffectiveScore(severity, lastSeen, recurrences, fileType, horizon
|
|
363
|
+
const future = computeEffectiveScore(severity, lastSeen, recurrences, fileType, horizon);
|
|
446
364
|
if (future < threshold)
|
|
447
365
|
agingRisk++;
|
|
448
366
|
}
|
|
@@ -454,24 +372,17 @@ function scanMistakePatternFile(filePath, threshold, fileType = 'mistake-pattern
|
|
|
454
372
|
for (const line of lines) {
|
|
455
373
|
if (scanned >= MAX_ENTRIES_SCANNED)
|
|
456
374
|
break;
|
|
457
|
-
const headerMatch = line.match(learningEntryHeadingRegex());
|
|
375
|
+
const headerMatch = line.match((0, learning_entry_format_1.learningEntryHeadingRegex)());
|
|
458
376
|
if (headerMatch) {
|
|
459
377
|
flush();
|
|
460
378
|
inEntry = true;
|
|
461
379
|
severity = headerMatch[1];
|
|
462
|
-
title = headerMatch[2].trim();
|
|
463
|
-
entryId = null;
|
|
464
380
|
lastSeen = '';
|
|
465
381
|
recurrences = 1;
|
|
466
382
|
continue;
|
|
467
383
|
}
|
|
468
384
|
if (!inEntry)
|
|
469
385
|
continue;
|
|
470
|
-
const idMatch = line.trim().match(LEARNING_ENTRY_ID_LINE);
|
|
471
|
-
if (idMatch) {
|
|
472
|
-
entryId = idMatch[1];
|
|
473
|
-
continue;
|
|
474
|
-
}
|
|
475
386
|
const lastSeenMatch = line.match(/^\*\*Last seen\*\*:\s*(.+)/);
|
|
476
387
|
if (lastSeenMatch) {
|
|
477
388
|
lastSeen = lastSeenMatch[1].trim();
|
|
@@ -485,22 +396,8 @@ function scanMistakePatternFile(filePath, threshold, fileType = 'mistake-pattern
|
|
|
485
396
|
flush();
|
|
486
397
|
return { active, dormant, agingRisk };
|
|
487
398
|
}
|
|
488
|
-
/**
|
|
489
|
-
|
|
490
|
-
* usage data at all. Returning undefined rather than an always-null function is
|
|
491
|
-
* deliberate: it keeps the "no usage record" path identical to pre-#1103
|
|
492
|
-
* behaviour instead of merely equivalent.
|
|
493
|
-
*/
|
|
494
|
-
function resolveUsageClock(workspaceRoot) {
|
|
495
|
-
return resolveUsageViews(workspaceRoot).clock;
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Issue #1755: one store read, two views of it: the clock the scorer needs and
|
|
499
|
-
* the counters a surface renders. They are resolved together because reading the
|
|
500
|
-
* store twice for the same pass would let the two disagree, and because the
|
|
501
|
-
* identity resolution that finds a record is the projection's, not each caller's.
|
|
502
|
-
*/
|
|
503
|
-
function resolveUsageViews(workspaceRoot) {
|
|
399
|
+
/** Historical counters are only read for the human library view. */
|
|
400
|
+
function resolveUsageStats() {
|
|
504
401
|
try {
|
|
505
402
|
// Required lazily: the store imports the retention config, which imports the
|
|
506
403
|
// workspace path helpers, and a cycle through this module would be fragile.
|
|
@@ -508,31 +405,28 @@ function resolveUsageViews(workspaceRoot) {
|
|
|
508
405
|
const projection = require('./learning-usage-projection');
|
|
509
406
|
const loaded = store.readUsageStore();
|
|
510
407
|
if (Object.keys(loaded.entries).length === 0)
|
|
511
|
-
return
|
|
408
|
+
return undefined;
|
|
512
409
|
const lookup = projection.buildUsageLookup(loaded);
|
|
513
|
-
return {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
lastFiredAt: found.lastFired,
|
|
528
|
-
};
|
|
529
|
-
},
|
|
410
|
+
return (id, fileType, title) => {
|
|
411
|
+
const found = projection.lookupUsage(lookup, id, fileType, title);
|
|
412
|
+
if (!found)
|
|
413
|
+
return null;
|
|
414
|
+
return {
|
|
415
|
+
historical: true,
|
|
416
|
+
countingStatus: 'deferred',
|
|
417
|
+
lastObservedAt: found.lastObservedAt,
|
|
418
|
+
applied: found.byOutcome.applied || 0,
|
|
419
|
+
prevented: found.byOutcome.prevented || 0,
|
|
420
|
+
ignored: found.byOutcome.ignored || 0,
|
|
421
|
+
notApplicable: found.byOutcome['not-applicable'] || 0,
|
|
422
|
+
lastFiredAt: found.lastFired,
|
|
423
|
+
};
|
|
530
424
|
};
|
|
531
425
|
}
|
|
532
426
|
catch {
|
|
533
427
|
// Usage data is derived and optional. A failure here must never change what
|
|
534
428
|
// the agent is given.
|
|
535
|
-
return
|
|
429
|
+
return undefined;
|
|
536
430
|
}
|
|
537
431
|
}
|
|
538
432
|
function readFrontmatter(content) {
|
|
@@ -552,34 +446,6 @@ function readFrontmatter(content) {
|
|
|
552
446
|
}
|
|
553
447
|
return frontmatter;
|
|
554
448
|
}
|
|
555
|
-
/**
|
|
556
|
-
* Issue #1857 R11: how many proposals are waiting for the manager's review.
|
|
557
|
-
*
|
|
558
|
-
* This counts directory entries and nothing else. It does not open a proposal, and
|
|
559
|
-
* deliberately has no access to one: R10 is an invariant about influence rather
|
|
560
|
-
* than storage, so the employee learns that a backlog exists and which job clears
|
|
561
|
-
* it, never what any pending claim says.
|
|
562
|
-
*
|
|
563
|
-
* The same shape as the pending-L0 line it sits beside, for the same reason: a
|
|
564
|
-
* count plus the job that clears it is actionable; a list of contents is not.
|
|
565
|
-
*/
|
|
566
|
-
function countPendingProposals(roots, resolvedUserId) {
|
|
567
|
-
let total = 0;
|
|
568
|
-
for (const base of [roots.repoLearningsBase, roots.globalPersonalBase]) {
|
|
569
|
-
if (!base)
|
|
570
|
-
continue;
|
|
571
|
-
const queueDir = (0, path_1.join)(base, 'review-queue', resolvedUserId);
|
|
572
|
-
if (!(0, fs_1.existsSync)(queueDir))
|
|
573
|
-
continue;
|
|
574
|
-
try {
|
|
575
|
-
total += (0, fs_1.readdirSync)(queueDir).filter((name) => name.endsWith('.json')).length;
|
|
576
|
-
}
|
|
577
|
-
catch {
|
|
578
|
-
// An unreadable queue is not worth failing a job's startup over.
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return total;
|
|
582
|
-
}
|
|
583
449
|
/** Configured owning homes only; discovery never provisions or infers a home. */
|
|
584
450
|
function getLearningInputHomes(workspaceRoot) {
|
|
585
451
|
const homes = [{ root: workspaceRoot, contentRoot: (0, path_1.dirname)((0, project_fraim_paths_1.getWorkspaceLearningsDir)(workspaceRoot)) },
|
|
@@ -736,15 +602,7 @@ const CONTEXT_CATEGORIES = {
|
|
|
736
602
|
coach: { ft: 'manager-coaching', gated: false, label: '(manager-facing; all entries)' },
|
|
737
603
|
validated: { ft: 'validated-patterns', gated: true, label: '(entries above score threshold)' },
|
|
738
604
|
};
|
|
739
|
-
/**
|
|
740
|
-
* Resolve exactly the files the injected context block lists, in the order it lists
|
|
741
|
-
* them.
|
|
742
|
-
*
|
|
743
|
-
* Extracted from `buildLearningContextSection` for issue #1103: the block renders
|
|
744
|
-
* these as text, and the offer recorder enumerates their entries. Both have to see
|
|
745
|
-
* the identical set, so there is one resolution and two consumers rather than two
|
|
746
|
-
* resolutions that can drift.
|
|
747
|
-
*/
|
|
605
|
+
/** Resolve the files listed in injected context, in delivery order. */
|
|
748
606
|
function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJob, domain) {
|
|
749
607
|
// Domain axis (issue #806) plus the connect/job single-injection contract (issue
|
|
750
608
|
// #860). Session startup stays broad enough for stable preferences plus L0, while
|
|
@@ -839,14 +697,13 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
|
839
697
|
const roots = getLearningRoots(workspaceRoot);
|
|
840
698
|
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
841
699
|
const threshold = getScoreThreshold(workspaceRoot);
|
|
842
|
-
const usageClock = resolveUsageClock(workspaceRoot);
|
|
843
700
|
const resolved = resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJob, domain);
|
|
844
701
|
const multiTier = resolved.multiTier;
|
|
845
702
|
const toListed = (f) => ({
|
|
846
703
|
displayPath: f.displayPath,
|
|
847
704
|
label: f.label,
|
|
848
705
|
dormant: f.gated
|
|
849
|
-
? scanMistakePatternFile(f.path, threshold, f.fileType
|
|
706
|
+
? scanMistakePatternFile(f.path, threshold, f.fileType).dormant
|
|
850
707
|
: 0,
|
|
851
708
|
isCoach: f.isCoach,
|
|
852
709
|
});
|
|
@@ -857,11 +714,9 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
|
857
714
|
const l0RetroCount = pendingL0Sources.filter(source => source.kind === 'retrospective').length;
|
|
858
715
|
const hasL2 = l2Files.length > 0;
|
|
859
716
|
const hasL1 = l1Files.length > 0;
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
|
|
863
|
-
const pendingProposals = countPendingProposals(roots, resolvedUserId);
|
|
864
|
-
const hasContent = hasL2 || hasL1 || l0CoachingCount > 0 || l0RetroCount > 0 || pendingProposals > 0;
|
|
717
|
+
// Saved proposal copies do not establish an outstanding human decision.
|
|
718
|
+
// The originating run's current review state owns reminders.
|
|
719
|
+
const hasContent = hasL2 || hasL1 || l0CoachingCount > 0 || l0RetroCount > 0;
|
|
865
720
|
if (!hasContent)
|
|
866
721
|
return '';
|
|
867
722
|
let section = forJob
|
|
@@ -903,10 +758,6 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
|
903
758
|
// Issue #1857 R11. The count, never the content: an unapproved proposal does
|
|
904
759
|
// not influence this job, and the employee is told only that the manager has a
|
|
905
760
|
// backlog and which job clears it.
|
|
906
|
-
if (pendingProposals > 0) {
|
|
907
|
-
const plural = pendingProposals !== 1;
|
|
908
|
-
section += `${pendingProposals} proposal${plural ? 's are' : ' is'} waiting for your manager's review; run \`sleep-on-learnings\` to review ${plural ? 'them' : 'it'}.\n\n`;
|
|
909
|
-
}
|
|
910
761
|
const coachPresent = resolved.coachPresent;
|
|
911
762
|
if (forJob) {
|
|
912
763
|
if (hasL2 || hasL1) {
|
|
@@ -930,159 +781,6 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
|
930
781
|
}
|
|
931
782
|
return section;
|
|
932
783
|
}
|
|
933
|
-
/** Enumerate one learning file's entries with their above-threshold state. */
|
|
934
|
-
function enumerateFileEntries(file, threshold, usageClock, now) {
|
|
935
|
-
if (!(0, fs_1.existsSync)(file.path))
|
|
936
|
-
return [];
|
|
937
|
-
let content;
|
|
938
|
-
try {
|
|
939
|
-
content = (0, fs_1.readFileSync)(file.path, 'utf8');
|
|
940
|
-
}
|
|
941
|
-
catch {
|
|
942
|
-
return [];
|
|
943
|
-
}
|
|
944
|
-
const out = [];
|
|
945
|
-
const headingRe = learningEntryHeadingRegex();
|
|
946
|
-
let current = null;
|
|
947
|
-
const flush = () => {
|
|
948
|
-
if (!current)
|
|
949
|
-
return;
|
|
950
|
-
// A non-gated family (preferences, manager-coaching) is delivered whole, so
|
|
951
|
-
// every entry in it is active guidance. A gated family delivers only entries
|
|
952
|
-
// at or above threshold; the rest are the dormant rollup.
|
|
953
|
-
let aboveThreshold = true;
|
|
954
|
-
if (file.gated) {
|
|
955
|
-
if (current.lastSeen) {
|
|
956
|
-
const lastFired = usageClock ? usageClock(current.id, file.fileType, current.title) : null;
|
|
957
|
-
aboveThreshold = computeEffectiveScore(current.severity, current.lastSeen, current.recurrences, file.fileType, now, lastFired) >= threshold;
|
|
958
|
-
}
|
|
959
|
-
// No date metadata: parseLearningEntries treats that as fresh and injects it,
|
|
960
|
-
// so the offer record has to agree.
|
|
961
|
-
}
|
|
962
|
-
out.push({
|
|
963
|
-
// The store owns key construction. Building the composite key by hand here
|
|
964
|
-
// would be a second definition of what an entry's identity is, and the two
|
|
965
|
-
// would drift the first time normalization changed. Issue #1752 A1 made
|
|
966
|
-
// that one function rather than four copies of the same expression.
|
|
967
|
-
key: (0, learning_usage_store_1.resolveEntryIdentity)({ id: current.id, fileType: file.fileType, title: current.title }).key,
|
|
968
|
-
family: 'learning',
|
|
969
|
-
id: current.id,
|
|
970
|
-
title: current.title,
|
|
971
|
-
fileType: file.fileType,
|
|
972
|
-
level: file.level,
|
|
973
|
-
displayPath: file.displayPath,
|
|
974
|
-
severity: current.severity,
|
|
975
|
-
aboveThreshold,
|
|
976
|
-
});
|
|
977
|
-
current = null;
|
|
978
|
-
};
|
|
979
|
-
for (const line of content.split(/\r?\n/)) {
|
|
980
|
-
const header = line.match(headingRe);
|
|
981
|
-
if (header) {
|
|
982
|
-
flush();
|
|
983
|
-
current = { severity: header[1], title: header[2].trim(), id: null, lastSeen: '', recurrences: 1 };
|
|
984
|
-
continue;
|
|
985
|
-
}
|
|
986
|
-
if (!current)
|
|
987
|
-
continue;
|
|
988
|
-
const trimmed = line.trim();
|
|
989
|
-
const idMatch = trimmed.match(LEARNING_ENTRY_ID_LINE);
|
|
990
|
-
if (idMatch) {
|
|
991
|
-
current.id = idMatch[1];
|
|
992
|
-
continue;
|
|
993
|
-
}
|
|
994
|
-
const lastSeenMatch = trimmed.match(/^\*\*Last seen\*\*:\s*(.+)/i);
|
|
995
|
-
if (lastSeenMatch) {
|
|
996
|
-
current.lastSeen = lastSeenMatch[1].trim();
|
|
997
|
-
continue;
|
|
998
|
-
}
|
|
999
|
-
const recurrenceMatch = trimmed.match(/^\*\*Recurrences\*\*:\s*(\d+)/i);
|
|
1000
|
-
if (recurrenceMatch) {
|
|
1001
|
-
current.recurrences = parseInt(recurrenceMatch[1], 10) || 1;
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
flush();
|
|
1005
|
-
return out;
|
|
1006
|
-
}
|
|
1007
|
-
/**
|
|
1008
|
-
* Issue #1103 R1 and R2: every learning entry the runtime just delivered.
|
|
1009
|
-
*
|
|
1010
|
-
* "Delivered" is file-level, because the injected block lists file paths rather
|
|
1011
|
-
* than entry titles: an entry is offered when it sat inside a file the block
|
|
1012
|
-
* listed. `aboveThreshold` is what separates active guidance from the dormant
|
|
1013
|
-
* rollup. Nothing here depends on the agent reading anything.
|
|
1014
|
-
*
|
|
1015
|
-
* Offers use stable identity, independently of ownership reconciliation. A repeated
|
|
1016
|
-
* ID records one offer, but equal titles with different IDs remain distinct.
|
|
1017
|
-
* Unstamped legacy entries retain their existing title usage key; that fallback
|
|
1018
|
-
* never grants a stamped entry authority over a different identity.
|
|
1019
|
-
*/
|
|
1020
|
-
function collectOfferedLearningEntries(workspaceRoot, userId, forJob, domain) {
|
|
1021
|
-
const roots = getLearningRoots(workspaceRoot);
|
|
1022
|
-
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
1023
|
-
const threshold = getScoreThreshold(workspaceRoot);
|
|
1024
|
-
const usageClock = resolveUsageClock(workspaceRoot);
|
|
1025
|
-
const now = new Date();
|
|
1026
|
-
const resolved = resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJob, domain);
|
|
1027
|
-
const byLesson = new Map();
|
|
1028
|
-
for (const file of [...resolved.l2, ...resolved.l1]) {
|
|
1029
|
-
for (const entry of enumerateFileEntries(file, threshold, usageClock, now)) {
|
|
1030
|
-
const lessonKey = entry.id ?? (0, learning_usage_store_1.titleUsageKey)(entry.fileType, entry.title);
|
|
1031
|
-
const group = byLesson.get(lessonKey);
|
|
1032
|
-
if (group)
|
|
1033
|
-
group.push(entry);
|
|
1034
|
-
else
|
|
1035
|
-
byLesson.set(lessonKey, [entry]);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
const out = [];
|
|
1039
|
-
for (const group of byLesson.values()) {
|
|
1040
|
-
// This is an offer counter, not a choice of authoritative guidance.
|
|
1041
|
-
const entry = group[0];
|
|
1042
|
-
out.push({ ...entry, aboveThreshold: group.some(item => item.aboveThreshold) });
|
|
1043
|
-
}
|
|
1044
|
-
return out;
|
|
1045
|
-
}
|
|
1046
|
-
/**
|
|
1047
|
-
* Issue #1103 R3: the auto-loaded Team Context rule files.
|
|
1048
|
-
*
|
|
1049
|
-
* These are the most influential instructions in the system and produce no usage
|
|
1050
|
-
* signal today, because the agent reads them with its own file tool and the proxy
|
|
1051
|
-
* never sees a `get_fraim_file` call for them. Recording the offer at composition
|
|
1052
|
-
* time closes that gap. A rule is unconditional, so it is always above threshold.
|
|
1053
|
-
*/
|
|
1054
|
-
function collectOfferedRuleFiles(workspaceRoot, forJob) {
|
|
1055
|
-
const resolved = resolveTeamContextFiles(workspaceRoot);
|
|
1056
|
-
// The three rule files by name, not by path sniffing: these are the ones R3
|
|
1057
|
-
// names, and the other Team Context files are context rather than instruction,
|
|
1058
|
-
// so recording them as offered guidance would misreport what they are.
|
|
1059
|
-
const ruleFiles = [
|
|
1060
|
-
{ presence: resolved.orgRules, level: 'org' },
|
|
1061
|
-
{ presence: resolved.managerRules, level: 'manager' },
|
|
1062
|
-
{ presence: resolved.projectRules, level: 'project' },
|
|
1063
|
-
];
|
|
1064
|
-
const out = [];
|
|
1065
|
-
for (const { presence, level } of ruleFiles) {
|
|
1066
|
-
if (!presence.present || !presence.displayPath)
|
|
1067
|
-
continue;
|
|
1068
|
-
const displayPath = presence.displayPath.replace(/\\/g, '/');
|
|
1069
|
-
out.push({
|
|
1070
|
-
key: (0, learning_usage_store_1.ruleUsageKey)(displayPath),
|
|
1071
|
-
family: 'rule',
|
|
1072
|
-
id: null,
|
|
1073
|
-
title: displayPath.split('/').pop() ?? displayPath,
|
|
1074
|
-
fileType: 'rules',
|
|
1075
|
-
level,
|
|
1076
|
-
displayPath,
|
|
1077
|
-
severity: null,
|
|
1078
|
-
aboveThreshold: true,
|
|
1079
|
-
});
|
|
1080
|
-
}
|
|
1081
|
-
// `forJob` is accepted so both injection points call this identically; the Team
|
|
1082
|
-
// Context block lists the same rule files in either frame.
|
|
1083
|
-
void forJob;
|
|
1084
|
-
return out;
|
|
1085
|
-
}
|
|
1086
784
|
/**
|
|
1087
785
|
* Resolve an organization/manager-scope context file (issue #563 order, R3.1):
|
|
1088
786
|
* 1. repo-local override (`fraim/personalized-employee/…`) — wins when present
|
|
@@ -1390,7 +1088,7 @@ function countLearningEntries(filePath) {
|
|
|
1390
1088
|
return 0;
|
|
1391
1089
|
}
|
|
1392
1090
|
let count = 0;
|
|
1393
|
-
const headingRe = learningEntryHeadingRegex();
|
|
1091
|
+
const headingRe = (0, learning_entry_format_1.learningEntryHeadingRegex)();
|
|
1394
1092
|
for (const line of content.split(/\r?\n/)) {
|
|
1395
1093
|
if (headingRe.test(line))
|
|
1396
1094
|
count++;
|
|
@@ -1413,7 +1111,7 @@ function readLearningEntryTitles(filePath) {
|
|
|
1413
1111
|
return [];
|
|
1414
1112
|
}
|
|
1415
1113
|
const titles = [];
|
|
1416
|
-
const headingRe = learningEntryHeadingRegex();
|
|
1114
|
+
const headingRe = (0, learning_entry_format_1.learningEntryHeadingRegex)();
|
|
1417
1115
|
for (const line of content.split(/\r?\n/)) {
|
|
1418
1116
|
const m = line.match(headingRe);
|
|
1419
1117
|
if (m)
|
|
@@ -1533,8 +1231,6 @@ const PENDING_KIND_LABEL = {
|
|
|
1533
1231
|
};
|
|
1534
1232
|
/** Filename infix that separates hot (.md) from cold-tier (.cold.md) learning files (#861). */
|
|
1535
1233
|
const COLD_FILE_SUFFIX = '.cold';
|
|
1536
|
-
/** R11's "fired recently" window. */
|
|
1537
|
-
const FIRED_RECENT_DAYS = 30;
|
|
1538
1234
|
// Parse `## [P-…] Title` entries out of a learning file WITH their effective
|
|
1539
1235
|
// score + last-seen (unlike parseLearningEntries, which drops that bookkeeping
|
|
1540
1236
|
// for the Hub text display). Used only by the brain-dot projection.
|
|
@@ -1549,7 +1245,7 @@ function parseBrainLearningEntries(filePath, fileType, usage) {
|
|
|
1549
1245
|
return [];
|
|
1550
1246
|
}
|
|
1551
1247
|
const out = [];
|
|
1552
|
-
const headingRe = learningEntryHeadingRegex();
|
|
1248
|
+
const headingRe = (0, learning_entry_format_1.learningEntryHeadingRegex)();
|
|
1553
1249
|
const now = new Date();
|
|
1554
1250
|
let current = null;
|
|
1555
1251
|
const flush = () => {
|
|
@@ -1557,7 +1253,7 @@ function parseBrainLearningEntries(filePath, fileType, usage) {
|
|
|
1557
1253
|
return;
|
|
1558
1254
|
const projected = usage ? usage(current.id, fileType, current.title) : undefined;
|
|
1559
1255
|
const rawScore = current.lastSeen
|
|
1560
|
-
? computeEffectiveScore(current.severity, current.lastSeen, current.recurrences, fileType, now
|
|
1256
|
+
? computeEffectiveScore(current.severity, current.lastSeen, current.recurrences, fileType, now)
|
|
1561
1257
|
: null;
|
|
1562
1258
|
out.push({
|
|
1563
1259
|
title: current.title,
|
|
@@ -1578,7 +1274,7 @@ function parseBrainLearningEntries(filePath, fileType, usage) {
|
|
|
1578
1274
|
}
|
|
1579
1275
|
if (!current)
|
|
1580
1276
|
continue;
|
|
1581
|
-
const idMatch = line.trim().match(LEARNING_ENTRY_ID_LINE);
|
|
1277
|
+
const idMatch = line.trim().match(learning_entry_format_1.LEARNING_ENTRY_ID_LINE);
|
|
1582
1278
|
if (idMatch) {
|
|
1583
1279
|
current.id = idMatch[1];
|
|
1584
1280
|
continue;
|
|
@@ -1609,26 +1305,19 @@ function resolveBrainUsageProjector() {
|
|
|
1609
1305
|
if (Object.keys(loaded.entries).length === 0)
|
|
1610
1306
|
return undefined;
|
|
1611
1307
|
const lookup = projection.buildUsageLookup(loaded);
|
|
1612
|
-
const now = Date.now();
|
|
1613
1308
|
return (id, fileType, title) => {
|
|
1614
1309
|
const value = projection.lookupUsage(lookup, id, fileType, title);
|
|
1615
1310
|
if (!value)
|
|
1616
1311
|
return undefined;
|
|
1617
|
-
let usageState;
|
|
1618
|
-
if (value.lastFired) {
|
|
1619
|
-
const ageDays = (now - new Date(value.lastFired).getTime()) / 86_400_000;
|
|
1620
|
-
usageState = ageDays <= FIRED_RECENT_DAYS ? 'fired-recent' : 'fired-aging';
|
|
1621
|
-
}
|
|
1622
|
-
else {
|
|
1623
|
-
usageState = 'never-fired';
|
|
1624
|
-
}
|
|
1625
1312
|
return {
|
|
1626
|
-
|
|
1313
|
+
historical: true,
|
|
1314
|
+
countingStatus: 'deferred',
|
|
1315
|
+
lastObservedAt: value.lastObservedAt,
|
|
1627
1316
|
fired: value.fired,
|
|
1628
1317
|
lastFired: value.lastFired,
|
|
1629
1318
|
firedUnder: value.firedUnder,
|
|
1630
1319
|
fireLog: value.log.map((l) => ({ date: l.date, outcome: l.outcome, job: l.job, note: l.note })),
|
|
1631
|
-
usageState,
|
|
1320
|
+
usageState: 'historical',
|
|
1632
1321
|
};
|
|
1633
1322
|
};
|
|
1634
1323
|
}
|
|
@@ -1726,7 +1415,7 @@ function listDomainLearningFiles(dir, prefix, fileType) {
|
|
|
1726
1415
|
// threshold: entries with score >= threshold (or in a non-gated category) are
|
|
1727
1416
|
// marked injected:true. Callers that don't need scoring pass Infinity so
|
|
1728
1417
|
// all entries are injected.
|
|
1729
|
-
function parseLearningEntries(filePath, displayPath, category, level, domain = 'global', tier, threshold = exports.DEFAULT_THRESHOLD,
|
|
1418
|
+
function parseLearningEntries(filePath, displayPath, category, level, domain = 'global', tier, threshold = exports.DEFAULT_THRESHOLD, usageStats) {
|
|
1730
1419
|
if (!(0, fs_1.existsSync)(filePath))
|
|
1731
1420
|
return [];
|
|
1732
1421
|
let content;
|
|
@@ -1748,14 +1437,11 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
|
|
|
1748
1437
|
const flush = () => {
|
|
1749
1438
|
if (current) {
|
|
1750
1439
|
current.body = bodyLines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
1751
|
-
//
|
|
1752
|
-
// counters a surface renders and the clock the score uses come from
|
|
1753
|
-
// the same record rather than two independent lookups.
|
|
1440
|
+
// Historical observations decorate the library; they never select guidance.
|
|
1754
1441
|
current.usage = usageStats ? usageStats(current.id, fileType, current.title) : null;
|
|
1755
1442
|
if (isScoreGated) {
|
|
1756
1443
|
if (curLastSeen !== null) {
|
|
1757
|
-
const
|
|
1758
|
-
const s = computeEffectiveScore(current.severity, curLastSeen, curRecurrences, fileType, new Date(), lastFired);
|
|
1444
|
+
const s = computeEffectiveScore(current.severity, curLastSeen, curRecurrences, fileType, new Date());
|
|
1759
1445
|
current.score = s;
|
|
1760
1446
|
current.injected = s >= threshold;
|
|
1761
1447
|
}
|
|
@@ -1778,7 +1464,7 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
|
|
|
1778
1464
|
curLastSeen = null;
|
|
1779
1465
|
curRecurrences = 1;
|
|
1780
1466
|
};
|
|
1781
|
-
const headingRe = learningEntryHeadingRegex();
|
|
1467
|
+
const headingRe = (0, learning_entry_format_1.learningEntryHeadingRegex)();
|
|
1782
1468
|
for (const line of content.split(/\r?\n/)) {
|
|
1783
1469
|
const header = line.match(headingRe);
|
|
1784
1470
|
if (header) {
|
|
@@ -1803,7 +1489,7 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
|
|
|
1803
1489
|
}
|
|
1804
1490
|
const t = line.trim();
|
|
1805
1491
|
// Capture scoring metadata but exclude from body.
|
|
1806
|
-
const idMatch = t.match(LEARNING_ENTRY_ID_LINE);
|
|
1492
|
+
const idMatch = t.match(learning_entry_format_1.LEARNING_ENTRY_ID_LINE);
|
|
1807
1493
|
if (idMatch) {
|
|
1808
1494
|
current.id = idMatch[1];
|
|
1809
1495
|
continue;
|
|
@@ -1849,6 +1535,7 @@ function readRejectedProposals(workspaceRoot, userId) {
|
|
|
1849
1535
|
return [];
|
|
1850
1536
|
const out = [];
|
|
1851
1537
|
const seen = new Set();
|
|
1538
|
+
const projectOwnerPath = (0, owner_identity_1.ownerPath)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot));
|
|
1852
1539
|
const headingRe = /^## (Rejected|Superseded): (.+)$/m;
|
|
1853
1540
|
const reasonRe = /^Reason: (.+)$/m;
|
|
1854
1541
|
const revisitRe = /^Revisit when: (.+)$/m;
|
|
@@ -1881,7 +1568,9 @@ function readRejectedProposals(workspaceRoot, userId) {
|
|
|
1881
1568
|
let proposal = {};
|
|
1882
1569
|
if (json) {
|
|
1883
1570
|
try {
|
|
1884
|
-
|
|
1571
|
+
const parsed = JSON.parse(json[1]);
|
|
1572
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
1573
|
+
proposal = parsed;
|
|
1885
1574
|
}
|
|
1886
1575
|
catch {
|
|
1887
1576
|
// Keep the human-readable half; the JSON only enriches it.
|
|
@@ -1890,12 +1579,24 @@ function readRejectedProposals(workspaceRoot, userId) {
|
|
|
1890
1579
|
const decision = (proposal.decision || {});
|
|
1891
1580
|
const content = (proposal.content || {});
|
|
1892
1581
|
const trace = (content.trace || {});
|
|
1582
|
+
let targetOwner;
|
|
1583
|
+
try {
|
|
1584
|
+
const owner = proposal.target?.owner;
|
|
1585
|
+
(0, owner_identity_1.validateOwner)(owner);
|
|
1586
|
+
targetOwner = { level: owner.level, path: owner.path };
|
|
1587
|
+
}
|
|
1588
|
+
catch {
|
|
1589
|
+
// Older/damaged targets remain visible as unknown; never infer
|
|
1590
|
+
// their project from the shared history file's location.
|
|
1591
|
+
}
|
|
1592
|
+
if (targetOwner?.level === 'Project' && targetOwner.path !== projectOwnerPath)
|
|
1593
|
+
continue;
|
|
1893
1594
|
const claimKey = typeof proposal.claimKey === 'string' && proposal.claimKey
|
|
1894
1595
|
? proposal.claimKey
|
|
1895
1596
|
: parts[1] || heading[2];
|
|
1896
1597
|
// The same claim can appear in more than one configured base. The first
|
|
1897
1598
|
// base supplies display metadata for that same recorded decision.
|
|
1898
|
-
const key =
|
|
1599
|
+
const key = JSON.stringify([claimKey, String(decision.at || ''), targetOwner?.level, targetOwner?.path]);
|
|
1899
1600
|
if (seen.has(key))
|
|
1900
1601
|
continue;
|
|
1901
1602
|
seen.add(key);
|
|
@@ -1910,6 +1611,7 @@ function readRejectedProposals(workspaceRoot, userId) {
|
|
|
1910
1611
|
rejectedAt: typeof decision.at === 'string' ? decision.at : '',
|
|
1911
1612
|
...(typeof trace.recurrences === 'number' ? { recurrencesAtRejection: trace.recurrences } : {}),
|
|
1912
1613
|
displayPath,
|
|
1614
|
+
...(targetOwner ? { targetOwner } : {}),
|
|
1913
1615
|
});
|
|
1914
1616
|
}
|
|
1915
1617
|
}
|
|
@@ -1919,16 +1621,44 @@ function readRejectedProposals(workspaceRoot, userId) {
|
|
|
1919
1621
|
function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine') {
|
|
1920
1622
|
const roots = getLearningRoots(workspaceRoot);
|
|
1921
1623
|
const threshold = getScoreThreshold(workspaceRoot);
|
|
1922
|
-
|
|
1923
|
-
//
|
|
1924
|
-
//
|
|
1925
|
-
const
|
|
1624
|
+
const usageStats = resolveUsageStats();
|
|
1625
|
+
// Only the human library reads these observations. An unavailable store must
|
|
1626
|
+
// fail this read, so a client can retain known history rather than invent zeroes.
|
|
1627
|
+
const resolvedUserId = scope === 'org' ? userId : resolveLearningUserId(workspaceRoot, userId, roots);
|
|
1628
|
+
const managerHome = (0, pack_home_1.resolvePackHome)('manager', { migrateLegacy: false });
|
|
1629
|
+
const orgHome = scope === 'org' ? (0, pack_home_1.resolvePackHome)('org', { migrateLegacy: false }) : null;
|
|
1630
|
+
const assessmentHomes = [
|
|
1631
|
+
{ level: 'Project', root: (0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot), contentRoot: (0, path_1.join)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot), 'personalized-employee') },
|
|
1632
|
+
{ level: 'Manager', root: managerHome.root, contentRoot: managerHome.contentRoot },
|
|
1633
|
+
...(orgHome ? [{ level: 'Org', root: orgHome.root, contentRoot: orgHome.contentRoot }] : []),
|
|
1634
|
+
];
|
|
1635
|
+
const requestedHomes = assessmentHomes.filter(home => scope === 'org'
|
|
1636
|
+
? home.level !== 'Manager' : home.level === (level === 'project' ? 'Project' : 'Manager'));
|
|
1637
|
+
const assessed = require('./learning-usage-store').readAssessedOutcomes(undefined, {
|
|
1638
|
+
owners: requestedHomes.map(home => ({ level: home.level, path: (0, owner_identity_1.ownerPath)(home.root) })), principal: userId,
|
|
1639
|
+
});
|
|
1640
|
+
const decorateAssessment = (entry, file) => {
|
|
1641
|
+
const home = assessmentHomes.find(candidate => {
|
|
1642
|
+
if (scope !== 'org' && candidate.level !== (entry.level === 'project' ? 'Project' : 'Manager'))
|
|
1643
|
+
return false;
|
|
1644
|
+
const child = (0, path_1.relative)(candidate.contentRoot, file);
|
|
1645
|
+
return child !== '..' && !child.startsWith('..' + path_1.sep) && !(0, path_1.isAbsolute)(child);
|
|
1646
|
+
});
|
|
1647
|
+
entry.assessment = home && entry.id ? (0, outcome_types_1.summarizeLearningOutcomes)(assessed, {
|
|
1648
|
+
owner: { level: home.level, path: (0, owner_identity_1.ownerPath)(home.root) },
|
|
1649
|
+
relativePath: (0, path_1.relative)(home.contentRoot, file).replace(/\\/g, '/'),
|
|
1650
|
+
// A legacy filename alias is not the authenticated learning subject.
|
|
1651
|
+
entryId: entry.id, learningSubject: userId,
|
|
1652
|
+
}) ?? null : null;
|
|
1653
|
+
return entry;
|
|
1654
|
+
};
|
|
1926
1655
|
const out = [];
|
|
1927
1656
|
// Helper: parse + merge a list of tier paths for one file family.
|
|
1928
1657
|
// ResolvedLearningTier uses 'manager' to mean machine-level personal; map it to 'machine'.
|
|
1929
1658
|
const tierLevel = (t) => t.level === 'manager' ? 'machine' : t.level;
|
|
1930
1659
|
const parseTiers = (tiers, cat, tierVal, domainVal) => mergeLearningEntriesAcrossTiers(tiers.map((t) => ({
|
|
1931
|
-
entries: parseLearningEntries(t.path, t.displayPath, cat, tierLevel(t), domainVal ?? 'global', tierVal, threshold,
|
|
1660
|
+
entries: parseLearningEntries(t.path, t.displayPath, cat, tierLevel(t), domainVal ?? 'global', tierVal, threshold, usageStats)
|
|
1661
|
+
.map(entry => decorateAssessment(entry, t.path)),
|
|
1932
1662
|
})));
|
|
1933
1663
|
if (scope === 'org') {
|
|
1934
1664
|
for (const cat of ['avoid', 'preference', 'repeat', 'coaching']) {
|
|
@@ -1949,7 +1679,8 @@ function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine')
|
|
|
1949
1679
|
seenDomainFile.add(fileName);
|
|
1950
1680
|
const domainTiers = resolveOrgLearningFileTiers(roots.repoLearningsBase, fileName);
|
|
1951
1681
|
if (domainTiers.length === 0) {
|
|
1952
|
-
out.push(...parseLearningEntries((0, path_1.join)(base, fileName), `${REPO_LEARNINGS_REL}/${fileName}`, cat, 'org', domain, undefined, threshold,
|
|
1682
|
+
out.push(...parseLearningEntries((0, path_1.join)(base, fileName), `${REPO_LEARNINGS_REL}/${fileName}`, cat, 'org', domain, undefined, threshold, usageStats)
|
|
1683
|
+
.map(entry => decorateAssessment(entry, (0, path_1.join)(base, fileName))));
|
|
1953
1684
|
}
|
|
1954
1685
|
else {
|
|
1955
1686
|
out.push(...parseTiers(domainTiers, cat, undefined, domain));
|
|
@@ -1959,7 +1690,6 @@ function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine')
|
|
|
1959
1690
|
return out;
|
|
1960
1691
|
}
|
|
1961
1692
|
// The library is an ownership view, not the combined runtime context.
|
|
1962
|
-
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
1963
1693
|
const cats = scope === 'reverse' ? ['coaching'] : ['avoid', 'preference', 'repeat'];
|
|
1964
1694
|
for (const cat of cats) {
|
|
1965
1695
|
const fileType = exports.CATEGORY_TO_FILETYPE[cat];
|
|
@@ -2016,7 +1746,7 @@ function applyLearningEntryChange(workspaceRoot, userId, ref, action, payload) {
|
|
|
2016
1746
|
const existing = (0, fs_1.existsSync)(filePath) ? (0, fs_1.readFileSync)(filePath, 'utf8') : '';
|
|
2017
1747
|
const nl = existing.includes('\r\n') ? '\r\n' : '\n';
|
|
2018
1748
|
const lines = existing.length ? existing.split(/\r?\n/) : [];
|
|
2019
|
-
const headingRe = learningEntryHeadingRegex();
|
|
1749
|
+
const headingRe = (0, learning_entry_format_1.learningEntryHeadingRegex)();
|
|
2020
1750
|
const starts = [];
|
|
2021
1751
|
lines.forEach((l, i) => { if (headingRe.test(l))
|
|
2022
1752
|
starts.push(i); });
|
|
@@ -2043,7 +1773,7 @@ function applyLearningEntryChange(workspaceRoot, userId, ref, action, payload) {
|
|
|
2043
1773
|
const matches = starts.map((start, index) => ({ start, index })).filter(({ start, index }) => {
|
|
2044
1774
|
if (payload.originalId !== undefined) {
|
|
2045
1775
|
const end = index + 1 < starts.length ? starts[index + 1] : lines.length;
|
|
2046
|
-
const ids = lines.slice(start + 1, end).map(line => line.trim().match(LEARNING_ENTRY_ID_LINE)).filter(Boolean);
|
|
1776
|
+
const ids = lines.slice(start + 1, end).map(line => line.trim().match(learning_entry_format_1.LEARNING_ENTRY_ID_LINE)).filter(Boolean);
|
|
2047
1777
|
return ids.length === 1 && ids[0][1] === payload.originalId;
|
|
2048
1778
|
}
|
|
2049
1779
|
return lines[start].match(headingRe)?.[2].trim() === (payload.originalTitle || '').trim();
|