fraim 2.0.211 → 2.0.212

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.
@@ -130,7 +130,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
130
130
  personaKey: 'ricardo',
131
131
  bundleId: 'persona-ricardo-core',
132
132
  catalogMetadata: buildCatalogMetadata('ricardo', ['role-intake-and-scorecard', 'job-posting-and-sourcing', 'candidate-pipeline-management']),
133
- protectedJobs: ['role-intake-and-scorecard', 'job-posting-and-sourcing', 'candidate-pipeline-management', 'interview-coordination', 'offer-and-close-management'],
133
+ protectedJobs: ['role-intake-and-scorecard', 'job-posting-and-sourcing', 'candidate-pipeline-management', 'sourcing-funnel-recap', 'interview-coordination', 'offer-and-close-management'],
134
134
  protectedAliases: ['recruiting', 'recruiter', 'hiring'],
135
135
  defaultHireMode: 'job',
136
136
  lockCopy: 'Hire RECardo to unlock full-cycle recruiting work for this request.'
@@ -6,7 +6,7 @@
6
6
  * workspace root on the user's machine.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.CATEGORY_TO_FILETYPE = exports.LEARNING_PRIORITIES = void 0;
9
+ exports.CATEGORY_TO_FILETYPE = exports.BRAIN_LEARNING_FILETYPE_TO_REGION = exports.LEARNING_PRIORITIES = void 0;
10
10
  exports.learningEntryHeadingRegex = learningEntryHeadingRegex;
11
11
  exports.computeEffectiveScore = computeEffectiveScore;
12
12
  exports.buildLearningContextSection = buildLearningContextSection;
@@ -17,6 +17,7 @@ exports.resolveTeamContextFile = resolveTeamContextFile;
17
17
  exports.readOrgBrand = readOrgBrand;
18
18
  exports.resolveOrgBrandWriteDir = resolveOrgBrandWriteDir;
19
19
  exports.countPreservedLearnings = countPreservedLearnings;
20
+ exports.collectBrainLearningDots = collectBrainLearningDots;
20
21
  exports.readPreservedLearnings = readPreservedLearnings;
21
22
  exports.applyLearningEntryChange = applyLearningEntryChange;
22
23
  exports.isTruthyFlag = isTruthyFlag;
@@ -385,11 +386,14 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
385
386
  coach: { ft: 'manager-coaching', gated: false, label: '(manager-facing; all entries)' },
386
387
  validated: { ft: 'validated-patterns', gated: true, label: '(entries above score threshold)' },
387
388
  };
388
- // Domain axis (issue #806): startup and job contexts both include the global
389
- // learning set. Job contexts may additionally append the current job's domain
390
- // files below the global files.
391
- const l2Cats = ['mistake', 'pref', 'coach', 'validated'];
392
- const l1Cats = ['pref', 'coach', 'mistake', 'validated'];
389
+ // Domain axis (issue #806): session startup stays broad enough for stable
390
+ // preferences plus L0, while job contexts load the full global set and may
391
+ // append the current job's domain files below the global files.
392
+ const sessionCats = ['pref'];
393
+ const jobL2Cats = ['mistake', 'pref', 'coach', 'validated'];
394
+ const jobL1Cats = ['pref', 'coach', 'mistake', 'validated'];
395
+ const l2Cats = forJob ? jobL2Cats : sessionCats;
396
+ const l1Cats = forJob ? jobL1Cats : sessionCats;
393
397
  const activeDomain = forJob && domain ? domain : null;
394
398
  const dormantOf = (meta, filePath) => meta.gated ? scanMistakePatternFile(filePath, threshold, meta.ft).dormant : 0;
395
399
  // Resolve one tier (L2 org or L1 personal) into ordered global + domain files.
@@ -473,14 +477,14 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
473
477
  const coachPresent = l1.coachPresent || l2.coachPresent;
474
478
  if (forJob) {
475
479
  if (hasL2 || hasL1) {
476
- section += 'Use the relevant patterns and preferences in this job.\n';
480
+ section += 'Read the listed synthesized learning files before continuing, then apply the relevant patterns and preferences in this job.\n';
477
481
  if (coachPresent) {
478
482
  section += 'Treat manager-coaching as feedback for how the manager should continue or improve managing AI, not as agent instruction.\n';
479
483
  }
480
484
  }
481
485
  }
482
- else {
483
- section += 'Use this synthesized learning context throughout the session.\n';
486
+ else if (hasL2 || hasL1) {
487
+ section += 'Read the listed synthesized learning files before continuing, then use this synthesized learning context throughout the session.\n';
484
488
  if (coachPresent) {
485
489
  section += 'Manager-coaching entries are manager-facing feedback, not instructions for the AI to follow.\n';
486
490
  }
@@ -840,6 +844,118 @@ function countPreservedLearnings(workspaceRoot, userId) {
840
844
  rawSignals,
841
845
  };
842
846
  }
847
+ // ── Issue #833: Brain learning-dot projection ──────────────────────────────
848
+ // Personal learning signals the FRAIM brain overlays on the SVG. Pending L0
849
+ // signals (raw coaching moments + unsynthesized retros) cluster in the
850
+ // temporal-memory region; processed L1/L2 learnings map to a fixed brain region
851
+ // per learning type (the approved Option A mapping). A read projection over the
852
+ // same files buildLearningContextSection injects — no new storage.
853
+ /** Fixed learning-type → brain-region mapping (issue #833, approved spec). */
854
+ exports.BRAIN_LEARNING_FILETYPE_TO_REGION = {
855
+ 'mistake-patterns': 'prefrontal-cortex', // rule following / constraint enforcement
856
+ 'preferences': 'right-parietal-experience', // experience / audience fit
857
+ 'validated-patterns': 'left-parietal-analysis', // engineering / evaluation judgment
858
+ 'manager-coaching': 'right-frontal-strategy', // strategy / framing (reverse mentoring)
859
+ };
860
+ const BRAIN_LEARNING_FILETYPE_LABEL = {
861
+ 'mistake-patterns': 'Mistake pattern',
862
+ 'preferences': 'Preference',
863
+ 'validated-patterns': 'Validated pattern',
864
+ 'manager-coaching': 'Manager coaching',
865
+ };
866
+ const PENDING_KIND_LABEL = {
867
+ 'coaching-moment': 'Coaching moment',
868
+ 'retrospective': 'Retrospective',
869
+ };
870
+ // Parse `## [P-…] Title` entries out of a learning file WITH their effective
871
+ // score + last-seen (unlike parseLearningEntries, which drops that bookkeeping
872
+ // for the Hub text display). Used only by the brain-dot projection.
873
+ function parseBrainLearningEntries(filePath, fileType) {
874
+ if (!(0, fs_1.existsSync)(filePath))
875
+ return [];
876
+ let content;
877
+ try {
878
+ content = (0, fs_1.readFileSync)(filePath, 'utf8');
879
+ }
880
+ catch {
881
+ return [];
882
+ }
883
+ const out = [];
884
+ const headingRe = learningEntryHeadingRegex();
885
+ const now = new Date();
886
+ let current = null;
887
+ const flush = () => {
888
+ if (!current)
889
+ return;
890
+ const rawScore = current.lastSeen
891
+ ? computeEffectiveScore(current.severity, current.lastSeen, current.recurrences, fileType, now)
892
+ : null;
893
+ out.push({
894
+ title: current.title,
895
+ severity: current.severity,
896
+ score: rawScore === null ? null : Math.round(rawScore * 10) / 10,
897
+ lastSeen: current.lastSeen || null,
898
+ });
899
+ current = null;
900
+ };
901
+ for (const line of content.split(/\r?\n/)) {
902
+ const header = line.match(headingRe);
903
+ if (header) {
904
+ flush();
905
+ current = { severity: header[1], title: header[2].trim(), lastSeen: '', recurrences: 1 };
906
+ continue;
907
+ }
908
+ if (!current)
909
+ continue;
910
+ const lastSeenMatch = line.match(/^\*\*Last seen\*\*:\s*(.+)/);
911
+ if (lastSeenMatch) {
912
+ current.lastSeen = lastSeenMatch[1].trim();
913
+ continue;
914
+ }
915
+ const recurrenceMatch = line.match(/^\*\*Recurrences\*\*:\s*(\d+)/);
916
+ if (recurrenceMatch) {
917
+ current.recurrences = parseInt(recurrenceMatch[1], 10);
918
+ }
919
+ }
920
+ flush();
921
+ return out;
922
+ }
923
+ /**
924
+ * Build the brain learning-dot projection (issue #833) for one user: pending L0
925
+ * signals + processed L1 (personal) and L2 (org) learnings grouped by file type
926
+ * and mapped to a fixed brain region. Reuses the same root/user resolution as
927
+ * buildLearningContextSection so the dots line up with the auto-loaded context.
928
+ */
929
+ function collectBrainLearningDots(workspaceRoot, userId) {
930
+ const roots = getLearningRoots(workspaceRoot);
931
+ const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
932
+ const pendingSignals = collectPendingL0SourceFiles(workspaceRoot, resolvedUserId, roots).map((s) => ({
933
+ type: s.kind,
934
+ typeLabel: PENDING_KIND_LABEL[s.kind],
935
+ fileName: s.displayPath.split('/').pop() || s.displayPath,
936
+ displayPath: s.displayPath,
937
+ }));
938
+ const resolvePersonal = (fileName) => resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, fileName);
939
+ const processed = [];
940
+ const fileTypes = ['mistake-patterns', 'preferences', 'validated-patterns', 'manager-coaching'];
941
+ for (const fileType of fileTypes) {
942
+ const region = exports.BRAIN_LEARNING_FILETYPE_TO_REGION[fileType];
943
+ const typeLabel = BRAIN_LEARNING_FILETYPE_LABEL[fileType];
944
+ // L1 personal
945
+ const personal = resolvePersonal(`${resolvedUserId}-${fileType}.md`);
946
+ const personalEntries = parseBrainLearningEntries(personal.path, fileType);
947
+ if (personalEntries.length) {
948
+ processed.push({ type: fileType, typeLabel, region, scope: 'personal', displayPath: personal.displayPath, entries: personalEntries });
949
+ }
950
+ // L2 org
951
+ const org = resolveOrgLearningFile(roots.repoLearningsBase, `org-${fileType}.md`);
952
+ const orgEntries = parseBrainLearningEntries(org.path, fileType);
953
+ if (orgEntries.length) {
954
+ processed.push({ type: fileType, typeLabel, region, scope: 'org', displayPath: org.displayPath, entries: orgEntries });
955
+ }
956
+ }
957
+ return { pending: { count: pendingSignals.length, signals: pendingSignals }, processed };
958
+ }
843
959
  exports.CATEGORY_TO_FILETYPE = {
844
960
  avoid: 'mistake-patterns',
845
961
  preference: 'preferences',
@@ -63,7 +63,7 @@ class TelemetryMiddleware {
63
63
  // session-active gate is exempted.
64
64
  // Issue #701/#744 - hosted AI Hub HTTP APIs are browser/gateway routes,
65
65
  // not MCP tool calls. They are still authenticated by API key/session.
66
- const exemptPaths = ['/health', '/admin', '/api/registry/sync', '/api/registry/github-workflows', '/api/registry/topology/personalized', '/api/registry/topology/usage-heatmap', '/api/org/', '/api/manager/', '/api/ai-hub/', '/api/payment/', '/api/sales/', '/api/signup', '/api/providers', '/api/analytics', '/analytics', '/api/auth/', '/api/account/'];
66
+ const exemptPaths = ['/health', '/admin', '/api/registry/sync', '/api/registry/github-workflows', '/api/registry/topology/personalized', '/api/registry/topology/usage-heatmap', '/api/registry/topology/learnings-dots', '/api/org/', '/api/manager/', '/api/ai-hub/', '/api/payment/', '/api/sales/', '/api/signup', '/api/providers', '/api/analytics', '/analytics', '/api/auth/', '/api/account/'];
67
67
  if (exemptPaths.some(p => req.path.startsWith(p))) {
68
68
  return next();
69
69
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.211",
3
+ "version": "2.0.212",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {