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.
- package/dist/cli.js +128 -1
- package/dist/hooks/elicitation-result.js +82 -1
- package/dist/hooks/post-tool-use.js +82 -1
- package/dist/hooks/pre-compact.js +30 -5
- package/dist/hooks/session-start.js +39 -11
- package/dist/hooks/stop.js +291 -14
- package/dist/server.js +360 -18
- package/package.json +1 -1
package/dist/hooks/stop.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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;
|