engrm 0.4.6 → 0.4.7

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.
@@ -2,6 +2,39 @@
2
2
  import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
+ // src/intelligence/observation-priority.ts
6
+ var RECENCY_WINDOW_SECONDS = 30 * 86400;
7
+ function computeBlendedScore(quality, createdAtEpoch, nowEpoch) {
8
+ const age = nowEpoch - createdAtEpoch;
9
+ const recencyNorm = Math.max(0, Math.min(1, 1 - age / RECENCY_WINDOW_SECONDS));
10
+ return quality * 0.6 + recencyNorm * 0.4;
11
+ }
12
+ function observationTypeBoost(type) {
13
+ switch (type) {
14
+ case "decision":
15
+ return 0.2;
16
+ case "pattern":
17
+ return 0.18;
18
+ case "bugfix":
19
+ return 0.14;
20
+ case "feature":
21
+ return 0.12;
22
+ case "discovery":
23
+ return 0.1;
24
+ case "refactor":
25
+ return 0.05;
26
+ case "digest":
27
+ return 0.03;
28
+ case "change":
29
+ return 0;
30
+ default:
31
+ return 0;
32
+ }
33
+ }
34
+ function computeObservationPriority(obs, nowEpoch) {
35
+ return computeBlendedScore(obs.quality, obs.created_at_epoch, nowEpoch) + observationTypeBoost(obs.type);
36
+ }
37
+
5
38
  // src/capture/retrospective.ts
6
39
  function extractRetrospective(observations, sessionId, projectId, userId) {
7
40
  if (observations.length === 0)
@@ -26,13 +59,16 @@ function extractRetrospective(observations, sessionId, projectId, userId) {
26
59
  };
27
60
  }
28
61
  function extractRequest(observations) {
62
+ const requestCandidate = observations.find((obs) => ["decision", "feature", "change", "bugfix", "discovery"].includes(obs.type) && obs.title.trim().length > 0 && !looksLikeFileOperation(obs.title));
63
+ if (requestCandidate)
64
+ return requestCandidate.title;
29
65
  const first = observations[0];
30
- if (!first)
66
+ if (!first || !first.title.trim())
31
67
  return null;
32
68
  return first.title;
33
69
  }
34
70
  function extractInvestigated(observations) {
35
- const discoveries = observations.filter((o) => o.type === "discovery");
71
+ const discoveries = observations.filter((o) => o.type === "discovery").sort((a, b) => scoreNarrativeObservation(b) - scoreNarrativeObservation(a));
36
72
  if (discoveries.length === 0)
37
73
  return null;
38
74
  return formatObservationGroup(discoveries, {
@@ -42,7 +78,7 @@ function extractInvestigated(observations) {
42
78
  }
43
79
  function extractLearned(observations) {
44
80
  const learnTypes = new Set(["bugfix", "decision", "pattern"]);
45
- const learned = observations.filter((o) => learnTypes.has(o.type));
81
+ const learned = observations.filter((o) => learnTypes.has(o.type)).sort((a, b) => scoreNarrativeObservation(b) - scoreNarrativeObservation(a));
46
82
  if (learned.length === 0)
47
83
  return null;
48
84
  return formatObservationGroup(learned, {
@@ -68,16 +104,18 @@ ${facts}` : `- ${title}`;
68
104
  function extractNextSteps(observations) {
69
105
  if (observations.length < 2)
70
106
  return null;
71
- const lastQuarterStart = Math.floor(observations.length * 0.75);
107
+ const lastQuarterStart = Math.max(0, Math.min(observations.length - 1, observations.length - 3, Math.floor(observations.length * 0.75)));
72
108
  const lastQuarter = observations.slice(lastQuarterStart);
73
109
  const unresolved = lastQuarter.filter((o) => o.type === "bugfix" && o.narrative && /error|fail|exception/i.test(o.narrative));
74
- if (unresolved.length === 0)
110
+ const explicitDecisions = lastQuarter.filter((o) => o.type === "decision").sort((a, b) => scoreNarrativeObservation(b) - scoreNarrativeObservation(a)).slice(0, 2).map((o) => `- Follow through: ${o.title}`);
111
+ if (unresolved.length === 0 && explicitDecisions.length === 0)
75
112
  return null;
76
- return unresolved.map((o) => `- Investigate: ${o.title}`).slice(0, 3).join(`
113
+ const lines = unresolved.map((o) => `- Investigate: ${o.title}`).slice(0, 3).concat(explicitDecisions);
114
+ return dedupeBulletLines(lines).join(`
77
115
  `);
78
116
  }
79
117
  function formatObservationGroup(observations, options) {
80
- const lines = dedupeObservationsByTitle(observations).slice(0, options.limit).map((o) => {
118
+ const lines = dedupeObservationsByTitle(observations).sort((a, b) => scoreNarrativeObservation(b) - scoreNarrativeObservation(a)).slice(0, options.limit).map((o) => {
81
119
  const facts = extractTopFacts(o, options.factsPerItem);
82
120
  return facts ? `- ${o.title}
83
121
  ${facts}` : `- ${o.title}`;
@@ -111,17 +149,24 @@ function dedupeObservationsByTitle(observations) {
111
149
  return deduped;
112
150
  }
113
151
  function scoreCompletedObservation(obs) {
114
- let score = obs.quality || 0;
152
+ let score = scoreNarrativeObservation(obs);
115
153
  if (obs.type === "feature")
116
154
  score += 0.5;
117
155
  if (obs.type === "refactor")
118
156
  score += 0.2;
119
- if (hasMeaningfulFacts(obs))
120
- score += 0.4;
121
157
  if (looksLikeFileOperation(obs.title))
122
158
  score -= 0.6;
159
+ return score;
160
+ }
161
+ function scoreNarrativeObservation(obs) {
162
+ const nowEpoch = Math.floor(Date.now() / 1000);
163
+ let score = computeObservationPriority(obs, nowEpoch);
164
+ if (hasMeaningfulFacts(obs))
165
+ score += 0.4;
123
166
  if (obs.narrative && obs.narrative.length > 80)
124
167
  score += 0.2;
168
+ if (looksLikeFileOperation(obs.title))
169
+ score -= 0.25;
125
170
  return score;
126
171
  }
127
172
  function hasMeaningfulFacts(obs) {
@@ -1506,6 +1551,31 @@ function markFailed(db, entryId, error) {
1506
1551
  WHERE id = ?`).run(error, now, entryId);
1507
1552
  }
1508
1553
 
1554
+ // src/intelligence/value-signals.ts
1555
+ var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
1556
+ function computeSessionValueSignals(observations, securityFindings = []) {
1557
+ const decisionsCount = observations.filter((o) => o.type === "decision").length;
1558
+ const lessonsCount = observations.filter((o) => LESSON_TYPES.has(o.type)).length;
1559
+ const discoveriesCount = observations.filter((o) => o.type === "discovery").length;
1560
+ const featuresCount = observations.filter((o) => o.type === "feature").length;
1561
+ const refactorsCount = observations.filter((o) => o.type === "refactor").length;
1562
+ const repeatedPatternsCount = observations.filter((o) => o.type === "pattern").length;
1563
+ const hasRequestSignal = observations.some((o) => ["feature", "decision", "change", "bugfix", "discovery"].includes(o.type));
1564
+ const hasCompletionSignal = observations.some((o) => ["feature", "change", "refactor", "bugfix"].includes(o.type));
1565
+ return {
1566
+ decisions_count: decisionsCount,
1567
+ lessons_count: lessonsCount,
1568
+ discoveries_count: discoveriesCount,
1569
+ features_count: featuresCount,
1570
+ refactors_count: refactorsCount,
1571
+ repeated_patterns_count: repeatedPatternsCount,
1572
+ security_findings_count: securityFindings.length,
1573
+ critical_security_findings_count: securityFindings.filter((f) => f.severity === "critical").length,
1574
+ delivery_review_ready: hasRequestSignal && hasCompletionSignal,
1575
+ vibe_guardian_active: securityFindings.length > 0
1576
+ };
1577
+ }
1578
+
1509
1579
  // src/sync/push.ts
1510
1580
  function buildVectorDocument(obs, config, project) {
1511
1581
  const parts = [obs.title];
@@ -1554,7 +1624,7 @@ function buildVectorDocument(obs, config, project) {
1554
1624
  }
1555
1625
  };
1556
1626
  }
1557
- function buildSummaryVectorDocument(summary, config, project) {
1627
+ function buildSummaryVectorDocument(summary, config, project, observations = []) {
1558
1628
  const parts = [];
1559
1629
  if (summary.request)
1560
1630
  parts.push(`Request: ${summary.request}`);
@@ -1566,6 +1636,7 @@ function buildSummaryVectorDocument(summary, config, project) {
1566
1636
  parts.push(`Completed: ${summary.completed}`);
1567
1637
  if (summary.next_steps)
1568
1638
  parts.push(`Next Steps: ${summary.next_steps}`);
1639
+ const valueSignals = computeSessionValueSignals(observations, []);
1569
1640
  return {
1570
1641
  site_id: config.site_id,
1571
1642
  namespace: config.namespace,
@@ -1584,6 +1655,19 @@ function buildSummaryVectorDocument(summary, config, project) {
1584
1655
  learned: summary.learned,
1585
1656
  completed: summary.completed,
1586
1657
  next_steps: summary.next_steps,
1658
+ summary_sections_present: countPresentSections(summary),
1659
+ investigated_items: extractSectionItems(summary.investigated),
1660
+ learned_items: extractSectionItems(summary.learned),
1661
+ completed_items: extractSectionItems(summary.completed),
1662
+ next_step_items: extractSectionItems(summary.next_steps),
1663
+ decisions_count: valueSignals.decisions_count,
1664
+ lessons_count: valueSignals.lessons_count,
1665
+ discoveries_count: valueSignals.discoveries_count,
1666
+ features_count: valueSignals.features_count,
1667
+ refactors_count: valueSignals.refactors_count,
1668
+ repeated_patterns_count: valueSignals.repeated_patterns_count,
1669
+ delivery_review_ready: valueSignals.delivery_review_ready,
1670
+ vibe_guardian_active: valueSignals.vibe_guardian_active,
1587
1671
  created_at_epoch: summary.created_at_epoch,
1588
1672
  local_id: summary.id
1589
1673
  }
@@ -1613,10 +1697,11 @@ async function pushOutbox(db, client, config, batchSize = 50) {
1613
1697
  continue;
1614
1698
  }
1615
1699
  markSyncing(db, entry.id);
1700
+ const summaryObservations = db.getObservationsBySession(summary.session_id);
1616
1701
  const doc2 = buildSummaryVectorDocument(summary, config, {
1617
1702
  canonical_id: project2.canonical_id,
1618
1703
  name: project2.name
1619
- });
1704
+ }, summaryObservations);
1620
1705
  batch.push({ entryId: entry.id, doc: doc2 });
1621
1706
  continue;
1622
1707
  }
@@ -1675,6 +1760,21 @@ async function pushOutbox(db, client, config, batchSize = 50) {
1675
1760
  }
1676
1761
  return { pushed, failed, skipped };
1677
1762
  }
1763
+ function countPresentSections(summary) {
1764
+ return [
1765
+ summary.request,
1766
+ summary.investigated,
1767
+ summary.learned,
1768
+ summary.completed,
1769
+ summary.next_steps
1770
+ ].filter((value) => Boolean(value && value.trim())).length;
1771
+ }
1772
+ function extractSectionItems(section) {
1773
+ if (!section)
1774
+ return [];
1775
+ return section.split(`
1776
+ `).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).slice(0, 4);
1777
+ }
1678
1778
 
1679
1779
  // src/embeddings/embedder.ts
1680
1780
  var _available = null;
@@ -1847,6 +1947,77 @@ function detectStacks(filePaths) {
1847
1947
  return Array.from(stacks).sort();
1848
1948
  }
1849
1949
 
1950
+ // src/intelligence/session-insights.ts
1951
+ function computeSessionInsights(summaries, observations) {
1952
+ const orderedSummaries = [...summaries].sort((a, b) => (b.created_at_epoch ?? 0) - (a.created_at_epoch ?? 0));
1953
+ const summaryCount = orderedSummaries.length;
1954
+ const summariesWithLearned = orderedSummaries.filter((s) => hasContent(s.learned)).length;
1955
+ const summariesWithCompleted = orderedSummaries.filter((s) => hasContent(s.completed)).length;
1956
+ const summariesWithNextSteps = orderedSummaries.filter((s) => hasContent(s.next_steps)).length;
1957
+ const totalSummarySectionsPresent = orderedSummaries.reduce((total, summary) => total + countPresentSections2(summary), 0);
1958
+ const recentRequests = dedupeLines(orderedSummaries.map((summary) => summary.request?.trim() ?? "").filter(Boolean)).slice(0, 3);
1959
+ const recentLessons = dedupeLines([
1960
+ ...orderedSummaries.flatMap((summary) => extractSectionItems2(summary.learned)),
1961
+ ...extractObservationTitles(observations, ["decision", "pattern", "bugfix"])
1962
+ ]).slice(0, 4);
1963
+ const recentCompleted = dedupeLines([
1964
+ ...orderedSummaries.flatMap((summary) => extractSectionItems2(summary.completed)),
1965
+ ...extractObservationTitles(observations, ["feature", "refactor", "change"])
1966
+ ]).slice(0, 4);
1967
+ const nextSteps = dedupeLines([
1968
+ ...orderedSummaries.flatMap((summary) => extractSectionItems2(summary.next_steps)),
1969
+ ...extractObservationTitles(observations, ["decision"]).map((title) => `Follow through: ${title}`)
1970
+ ]).slice(0, 4);
1971
+ return {
1972
+ summary_count: summaryCount,
1973
+ summaries_with_learned: summariesWithLearned,
1974
+ summaries_with_completed: summariesWithCompleted,
1975
+ summaries_with_next_steps: summariesWithNextSteps,
1976
+ total_summary_sections_present: totalSummarySectionsPresent,
1977
+ recent_requests: recentRequests,
1978
+ recent_lessons: recentLessons,
1979
+ recent_completed: recentCompleted,
1980
+ next_steps: nextSteps
1981
+ };
1982
+ }
1983
+ function countPresentSections2(summary) {
1984
+ return [
1985
+ summary.request,
1986
+ summary.investigated,
1987
+ summary.learned,
1988
+ summary.completed,
1989
+ summary.next_steps
1990
+ ].filter(hasContent).length;
1991
+ }
1992
+ function extractSectionItems2(section) {
1993
+ if (!hasContent(section))
1994
+ return [];
1995
+ return section.split(`
1996
+ `).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
1997
+ }
1998
+ function extractObservationTitles(observations, types) {
1999
+ const typeSet = new Set(types);
2000
+ return observations.filter((obs) => typeSet.has(obs.type)).sort((a, b) => b.created_at_epoch - a.created_at_epoch).map((obs) => obs.title.trim()).filter(Boolean);
2001
+ }
2002
+ function dedupeLines(lines) {
2003
+ const seen = new Set;
2004
+ const result = [];
2005
+ for (const line of lines) {
2006
+ const cleaned = line.replace(/\s+/g, " ").trim();
2007
+ if (!cleaned)
2008
+ continue;
2009
+ const key = cleaned.toLowerCase();
2010
+ if (seen.has(key))
2011
+ continue;
2012
+ seen.add(key);
2013
+ result.push(cleaned);
2014
+ }
2015
+ return result;
2016
+ }
2017
+ function hasContent(value) {
2018
+ return Boolean(value && value.trim());
2019
+ }
2020
+
1850
2021
  // src/telemetry/beacon.ts
1851
2022
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
1852
2023
  import { join as join3 } from "node:path";
@@ -1877,6 +2048,17 @@ function buildBeacon(db, config, sessionId, metrics) {
1877
2048
  for (const obs of observations) {
1878
2049
  byType[obs.type] = (byType[obs.type] ?? 0) + 1;
1879
2050
  }
2051
+ let securityFindings = [];
2052
+ try {
2053
+ if (session.project_id) {
2054
+ securityFindings = db.getSecurityFindings(session.project_id, { limit: 200 }).filter((f) => f.session_id === sessionId);
2055
+ }
2056
+ } catch {
2057
+ securityFindings = [];
2058
+ }
2059
+ const valueSignals = computeSessionValueSignals(observations, securityFindings);
2060
+ const summaries = session.project_id ? db.getRecentSummaries(session.project_id, 20).filter((summary) => summary.session_id === sessionId) : [];
2061
+ const sessionInsights = computeSessionInsights(summaries, observations);
1880
2062
  const filePaths = [];
1881
2063
  for (const obs of observations) {
1882
2064
  if (obs.files_modified) {
@@ -1926,7 +2108,7 @@ function buildBeacon(db, config, sessionId, metrics) {
1926
2108
  observer_events: observerEvents,
1927
2109
  observer_observations: observerObservations,
1928
2110
  observer_skips: observerSkips,
1929
- sentinel_used: false,
2111
+ sentinel_used: valueSignals.security_findings_count > 0,
1930
2112
  risk_score: riskScore,
1931
2113
  stacks_detected: stacks,
1932
2114
  client_version: "0.4.0",
@@ -1936,6 +2118,20 @@ function buildBeacon(db, config, sessionId, metrics) {
1936
2118
  recall_hits: metrics?.recallHits ?? 0,
1937
2119
  search_count: metrics?.searchCount ?? 0,
1938
2120
  search_results_total: metrics?.searchResultsTotal ?? 0,
2121
+ decisions_count: valueSignals.decisions_count,
2122
+ lessons_count: valueSignals.lessons_count,
2123
+ discoveries_count: valueSignals.discoveries_count,
2124
+ features_count: valueSignals.features_count,
2125
+ refactors_count: valueSignals.refactors_count,
2126
+ repeated_patterns_count: valueSignals.repeated_patterns_count,
2127
+ security_findings_count: valueSignals.security_findings_count,
2128
+ critical_security_findings_count: valueSignals.critical_security_findings_count,
2129
+ delivery_review_ready: valueSignals.delivery_review_ready,
2130
+ vibe_guardian_active: valueSignals.vibe_guardian_active,
2131
+ summaries_with_learned: sessionInsights.summaries_with_learned,
2132
+ summaries_with_completed: sessionInsights.summaries_with_completed,
2133
+ summaries_with_next_steps: sessionInsights.summaries_with_next_steps,
2134
+ total_summary_sections_present: sessionInsights.total_summary_sections_present,
1939
2135
  config_hash: configHash,
1940
2136
  config_changed: configChanged,
1941
2137
  config_fingerprint_detail: configFingerprintDetail
@@ -2315,6 +2511,80 @@ function findDuplicate(newTitle, candidates) {
2315
2511
  return bestMatch;
2316
2512
  }
2317
2513
 
2514
+ // src/capture/facts.ts
2515
+ var FACT_ELIGIBLE_TYPES = new Set([
2516
+ "bugfix",
2517
+ "decision",
2518
+ "discovery",
2519
+ "pattern",
2520
+ "feature",
2521
+ "refactor",
2522
+ "change"
2523
+ ]);
2524
+ function buildStructuredFacts(input) {
2525
+ const seedFacts = dedupeFacts(input.facts ?? []);
2526
+ if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
2527
+ return seedFacts;
2528
+ }
2529
+ const derived = [...seedFacts];
2530
+ if (seedFacts.length === 0 && looksMeaningful(input.title)) {
2531
+ derived.push(input.title.trim());
2532
+ }
2533
+ for (const sentence of extractNarrativeFacts(input.narrative)) {
2534
+ derived.push(sentence);
2535
+ }
2536
+ const fileFact = buildFilesFact(input.filesModified);
2537
+ if (fileFact) {
2538
+ derived.push(fileFact);
2539
+ }
2540
+ return dedupeFacts(derived).slice(0, 4);
2541
+ }
2542
+ function extractNarrativeFacts(narrative) {
2543
+ if (!narrative)
2544
+ return [];
2545
+ const cleaned = narrative.replace(/\s+/g, " ").trim();
2546
+ if (cleaned.length < 24)
2547
+ return [];
2548
+ const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
2549
+ return parts.slice(0, 2);
2550
+ }
2551
+ function buildFilesFact(filesModified) {
2552
+ if (!filesModified || filesModified.length === 0)
2553
+ return null;
2554
+ const cleaned = filesModified.map((file) => file.trim()).filter(Boolean).slice(0, 3);
2555
+ if (cleaned.length === 0)
2556
+ return null;
2557
+ if (cleaned.length === 1) {
2558
+ return `Touched ${cleaned[0]}`;
2559
+ }
2560
+ return `Touched ${cleaned.join(", ")}`;
2561
+ }
2562
+ function dedupeFacts(facts) {
2563
+ const seen = new Set;
2564
+ const result = [];
2565
+ for (const fact of facts) {
2566
+ const cleaned = fact.trim().replace(/\s+/g, " ");
2567
+ if (!looksMeaningful(cleaned))
2568
+ continue;
2569
+ const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
2570
+ if (!key || seen.has(key))
2571
+ continue;
2572
+ seen.add(key);
2573
+ result.push(cleaned);
2574
+ }
2575
+ return result;
2576
+ }
2577
+ function looksMeaningful(value) {
2578
+ const cleaned = value.trim();
2579
+ if (cleaned.length < 12)
2580
+ return false;
2581
+ if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
2582
+ return false;
2583
+ if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
2584
+ return false;
2585
+ return true;
2586
+ }
2587
+
2318
2588
  // src/capture/recurrence.ts
2319
2589
  var DISTANCE_THRESHOLD = 0.15;
2320
2590
  async function detectRecurrence(db, config, observation) {
@@ -2532,10 +2802,17 @@ async function saveObservation(db, config, input) {
2532
2802
  const customPatterns = config.scrubbing.enabled ? config.scrubbing.custom_patterns : [];
2533
2803
  const title = config.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
2534
2804
  const narrative = input.narrative ? config.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
2535
- const factsJson = input.facts ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
2536
2805
  const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
2537
2806
  const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
2538
2807
  const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
2808
+ const structuredFacts = buildStructuredFacts({
2809
+ type: input.type,
2810
+ title: input.title,
2811
+ narrative: input.narrative,
2812
+ facts: input.facts,
2813
+ filesModified
2814
+ });
2815
+ const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
2539
2816
  const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
2540
2817
  const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
2541
2818
  let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;