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.
Files changed (34) hide show
  1. package/dist/registry/scripts/learning/completion-state.js +80 -4
  2. package/dist/registry/scripts/learning/corpus-audit.js +7 -7
  3. package/dist/registry/scripts/learning/entry-authoring.js +181 -0
  4. package/dist/registry/scripts/learning/entry-format.js +10 -1
  5. package/dist/registry/scripts/learning/outcome-assessment.js +162 -0
  6. package/dist/registry/scripts/learning/outcome-types.js +92 -0
  7. package/dist/registry/scripts/learning/owner-identity.js +11 -0
  8. package/dist/registry/scripts/learning/proposal-queue.js +81 -0
  9. package/dist/registry/scripts/learning/run-application.js +1003 -0
  10. package/dist/registry/scripts/learning/run-publication.js +364 -0
  11. package/dist/registry/scripts/learning/run-record.js +895 -0
  12. package/dist/registry/scripts/learning-usage/learning-source-record.js +131 -0
  13. package/dist/src/cli/commands/learning.js +64 -0
  14. package/dist/src/cli/doctor/checks/scripts-checks.js +53 -1
  15. package/dist/src/cli/fraim.js +2 -0
  16. package/dist/src/cli/learning/inventory.js +285 -0
  17. package/dist/src/cli/learning/queue-operations.js +199 -0
  18. package/dist/src/cli/learning/runtime.js +82 -0
  19. package/dist/src/cli/learning/usage.js +277 -0
  20. package/dist/src/core/fraim-config-schema.generated.js +0 -4
  21. package/dist/src/core/utils/local-registry-resolver.js +13 -3
  22. package/dist/src/local-mcp-server/learning-context-builder.js +109 -379
  23. package/dist/src/local-mcp-server/learning-entry-format.js +25 -0
  24. package/dist/src/local-mcp-server/learning-usage-analysis.js +12 -410
  25. package/dist/src/local-mcp-server/learning-usage-eligibility.js +8 -14
  26. package/dist/src/local-mcp-server/learning-usage-projection.js +5 -22
  27. package/dist/src/local-mcp-server/learning-usage-store.js +93 -393
  28. package/dist/src/local-mcp-server/learning-usage-verify.js +9 -40
  29. package/dist/src/local-mcp-server/stdio-server.js +50 -114
  30. package/dist/src/mcp/tool-schemas.js +2 -1
  31. package/package.json +9 -1
  32. package/dist/src/local-mcp-server/learning-firing-parser.js +0 -247
  33. package/dist/src/local-mcp-server/learning-usage-attestation.js +0 -238
  34. 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
- exports.LEARNING_PRIORITIES = ['P-CRITICAL', 'P-HIGH', 'P-MED', 'P-LOW'];
74
- /** Canonical entry-heading matcher: `##`..`######` + `[P-X]` + optional title. */
75
- function learningEntryHeadingRegex(flags = '') {
76
- return new RegExp(`^#{2,}\\s+\\[(${exports.LEARNING_PRIORITIES.join('|')})\\]\\s*(.*)$`, flags);
77
- }
78
- /**
79
- * Issue #1103 R5: the entry's stable identifier, written as a `**Id**: L-…` line.
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(), lastFiredDate) {
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
- // Issue #1103 R8: the clock is max(last_seen, last_fired). `Last seen` only
365
- // advances when the same signal recurs, which for a mistake pattern means the
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
- /** Clock skew tolerance for a firing date, in milliseconds. */
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 lastFired = usageClock ? usageClock(entryId, fileType, title) : null;
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, lastFired);
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
- * Build the usage clock from the store, or return undefined when there is no
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
- clock: (id, fileType, title) => projection.lookupUsage(lookup, id, fileType, title)?.lastFired ?? null,
515
- stats: (id, fileType, title) => {
516
- const found = projection.lookupUsage(lookup, id, fileType, title);
517
- if (!found)
518
- return null;
519
- return {
520
- offered: found.offered,
521
- offeredAboveThreshold: found.offeredAboveThreshold,
522
- applied: found.byOutcome.applied || 0,
523
- prevented: found.byOutcome.prevented || 0,
524
- ignored: found.byOutcome.ignored || 0,
525
- notApplicable: found.byOutcome['not-applicable'] || 0,
526
- lastOfferedAt: found.lastOffered,
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, usageClock).dormant
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
- // Issue #1857 R11. Counted before the gate on purpose: a manager with a full
861
- // review queue and an empty corpus is exactly the case where the backlog most
862
- // needs saying, and computing it after the early return would silence it there.
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, projected?.lastFired ?? null)
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
- offered: value.offered,
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, usageClock, usageStats) {
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
- // Issue #1755: resolve usage once per entry, before scoring, so the
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 lastFired = usageClock ? usageClock(current.id, fileType, current.title) : null;
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
- proposal = JSON.parse(json[1]);
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 = `${claimKey}:${String(decision.at || '')}`;
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
- // Issue #1103 R8: the Hub's injected flag must agree with the loader's, so both
1923
- // read the same clock. One store read serves the whole call.
1924
- // Issue #1755: the same read also yields the counters the Hub renders.
1925
- const { clock: usageClock, stats: usageStats } = resolveUsageViews(workspaceRoot);
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, usageClock, usageStats),
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, usageClock, usageStats));
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();