@mukulaggarwal/pacman 0.1.0
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/README.md +39 -0
- package/dist/chunk-3QNXXON5.js +330 -0
- package/dist/chunk-3QNXXON5.js.map +1 -0
- package/dist/chunk-43PUZDIZ.js +148 -0
- package/dist/chunk-43PUZDIZ.js.map +1 -0
- package/dist/chunk-7D4SUZUM.js +38 -0
- package/dist/chunk-7D4SUZUM.js.map +1 -0
- package/dist/chunk-AYFIQNZ5.js +807 -0
- package/dist/chunk-AYFIQNZ5.js.map +1 -0
- package/dist/chunk-FH6ZHWGR.js +37 -0
- package/dist/chunk-FH6ZHWGR.js.map +1 -0
- package/dist/chunk-O6T35A4O.js +137 -0
- package/dist/chunk-O6T35A4O.js.map +1 -0
- package/dist/chunk-TRQIZP6Z.js +451 -0
- package/dist/chunk-TRQIZP6Z.js.map +1 -0
- package/dist/chunk-UWT6AFJB.js +471 -0
- package/dist/chunk-UWT6AFJB.js.map +1 -0
- package/dist/chunk-ZKKMIDRK.js +3923 -0
- package/dist/chunk-ZKKMIDRK.js.map +1 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +141 -0
- package/dist/daemon.js.map +1 -0
- package/dist/dist-3PIJOFZ4.js +91 -0
- package/dist/dist-3PIJOFZ4.js.map +1 -0
- package/dist/dist-L76NGFFH.js +102 -0
- package/dist/dist-L76NGFFH.js.map +1 -0
- package/dist/dist-NV2YVVHI.js +178 -0
- package/dist/dist-NV2YVVHI.js.map +1 -0
- package/dist/dist-RMYCRZIU.js +41 -0
- package/dist/dist-RMYCRZIU.js.map +1 -0
- package/dist/dist-THLCZNOZ.js +14 -0
- package/dist/dist-THLCZNOZ.js.map +1 -0
- package/dist/dist-TWNHTXYH.js +95 -0
- package/dist/dist-TWNHTXYH.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +452 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-compat.d.ts +1 -0
- package/dist/mcp-compat.js +78 -0
- package/dist/mcp-compat.js.map +1 -0
- package/dist/onboarding-server.d.ts +3 -0
- package/dist/onboarding-server.js +1172 -0
- package/dist/onboarding-server.js.map +1 -0
- package/dist/provider-runtime.d.ts +11 -0
- package/dist/provider-runtime.js +10 -0
- package/dist/provider-runtime.js.map +1 -0
- package/dist/slack-listener.d.ts +49 -0
- package/dist/slack-listener.js +888 -0
- package/dist/slack-listener.js.map +1 -0
- package/dist/storage.d.ts +8 -0
- package/dist/storage.js +9 -0
- package/dist/storage.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContextManager
|
|
3
|
+
} from "./chunk-UWT6AFJB.js";
|
|
4
|
+
import {
|
|
5
|
+
generateDraft
|
|
6
|
+
} from "./chunk-O6T35A4O.js";
|
|
7
|
+
import {
|
|
8
|
+
resolveConfiguredStorage
|
|
9
|
+
} from "./chunk-FH6ZHWGR.js";
|
|
10
|
+
import {
|
|
11
|
+
WORKSPACE_PATHS
|
|
12
|
+
} from "./chunk-TRQIZP6Z.js";
|
|
13
|
+
import {
|
|
14
|
+
createSlackConnector,
|
|
15
|
+
openSocketModeConnection
|
|
16
|
+
} from "./chunk-ZKKMIDRK.js";
|
|
17
|
+
import "./chunk-7D4SUZUM.js";
|
|
18
|
+
|
|
19
|
+
// src/slack-listener.ts
|
|
20
|
+
import { google } from "googleapis";
|
|
21
|
+
var RECONNECT_DELAY_MS = 5e3;
|
|
22
|
+
var RECENT_EVENT_TTL_MS = 5 * 60 * 1e3;
|
|
23
|
+
var PROJECT_RESOLUTION_MIN_SCORE = 0.45;
|
|
24
|
+
var PROJECT_RESOLUTION_MIN_MARGIN = 0.08;
|
|
25
|
+
var recentEventKeys = /* @__PURE__ */ new Map();
|
|
26
|
+
var RESOLUTION_STOPWORDS = /* @__PURE__ */ new Set([
|
|
27
|
+
"a",
|
|
28
|
+
"about",
|
|
29
|
+
"after",
|
|
30
|
+
"again",
|
|
31
|
+
"all",
|
|
32
|
+
"also",
|
|
33
|
+
"an",
|
|
34
|
+
"and",
|
|
35
|
+
"any",
|
|
36
|
+
"are",
|
|
37
|
+
"as",
|
|
38
|
+
"at",
|
|
39
|
+
"back",
|
|
40
|
+
"be",
|
|
41
|
+
"been",
|
|
42
|
+
"before",
|
|
43
|
+
"being",
|
|
44
|
+
"between",
|
|
45
|
+
"both",
|
|
46
|
+
"but",
|
|
47
|
+
"by",
|
|
48
|
+
"can",
|
|
49
|
+
"could",
|
|
50
|
+
"date",
|
|
51
|
+
"did",
|
|
52
|
+
"do",
|
|
53
|
+
"does",
|
|
54
|
+
"effective",
|
|
55
|
+
"for",
|
|
56
|
+
"from",
|
|
57
|
+
"had",
|
|
58
|
+
"has",
|
|
59
|
+
"have",
|
|
60
|
+
"help",
|
|
61
|
+
"here",
|
|
62
|
+
"how",
|
|
63
|
+
"i",
|
|
64
|
+
"if",
|
|
65
|
+
"in",
|
|
66
|
+
"into",
|
|
67
|
+
"is",
|
|
68
|
+
"it",
|
|
69
|
+
"its",
|
|
70
|
+
"just",
|
|
71
|
+
"latest",
|
|
72
|
+
"me",
|
|
73
|
+
"more",
|
|
74
|
+
"most",
|
|
75
|
+
"my",
|
|
76
|
+
"need",
|
|
77
|
+
"of",
|
|
78
|
+
"on",
|
|
79
|
+
"only",
|
|
80
|
+
"or",
|
|
81
|
+
"other",
|
|
82
|
+
"our",
|
|
83
|
+
"out",
|
|
84
|
+
"over",
|
|
85
|
+
"same",
|
|
86
|
+
"share",
|
|
87
|
+
"should",
|
|
88
|
+
"show",
|
|
89
|
+
"tell",
|
|
90
|
+
"than",
|
|
91
|
+
"that",
|
|
92
|
+
"the",
|
|
93
|
+
"their",
|
|
94
|
+
"them",
|
|
95
|
+
"there",
|
|
96
|
+
"these",
|
|
97
|
+
"they",
|
|
98
|
+
"this",
|
|
99
|
+
"those",
|
|
100
|
+
"through",
|
|
101
|
+
"to",
|
|
102
|
+
"under",
|
|
103
|
+
"update",
|
|
104
|
+
"us",
|
|
105
|
+
"using",
|
|
106
|
+
"want",
|
|
107
|
+
"was",
|
|
108
|
+
"we",
|
|
109
|
+
"what",
|
|
110
|
+
"when",
|
|
111
|
+
"where",
|
|
112
|
+
"which",
|
|
113
|
+
"while",
|
|
114
|
+
"who",
|
|
115
|
+
"why",
|
|
116
|
+
"with",
|
|
117
|
+
"would",
|
|
118
|
+
"you",
|
|
119
|
+
"your"
|
|
120
|
+
]);
|
|
121
|
+
async function startSlackListener(workspacePath) {
|
|
122
|
+
const { config, storage } = await resolveConfiguredStorage(workspacePath);
|
|
123
|
+
const slackRuntime = config.slackRuntime;
|
|
124
|
+
if (!slackRuntime?.enabled) {
|
|
125
|
+
throw new Error("Slack runtime is not enabled in the saved configuration");
|
|
126
|
+
}
|
|
127
|
+
if (slackRuntime.transport !== "socket_mode") {
|
|
128
|
+
throw new Error(`Slack transport "${slackRuntime.transport}" is not supported yet. Use Socket Mode for v1.`);
|
|
129
|
+
}
|
|
130
|
+
const slackIntegration = config.integrations.find((integration) => integration.type === "slack" && integration.enabled);
|
|
131
|
+
if (!slackIntegration?.credentials?.botToken) {
|
|
132
|
+
throw new Error("Slack integration is enabled but the bot token is missing");
|
|
133
|
+
}
|
|
134
|
+
if (!slackRuntime.appToken) {
|
|
135
|
+
throw new Error("Slack runtime app token is missing");
|
|
136
|
+
}
|
|
137
|
+
const deps = {
|
|
138
|
+
config,
|
|
139
|
+
storage,
|
|
140
|
+
slackIntegration,
|
|
141
|
+
slackRuntime
|
|
142
|
+
};
|
|
143
|
+
const connector = createSlackConnector();
|
|
144
|
+
await connector.authenticate(slackIntegration);
|
|
145
|
+
console.log("Starting real-time Slack listener...");
|
|
146
|
+
console.log(`Transport: ${slackRuntime.transport}`);
|
|
147
|
+
console.log(`Review mode: ${slackRuntime.reviewMode}`);
|
|
148
|
+
console.log(`Generation enabled: ${slackRuntime.generationEnabled ? "yes" : "no"}`);
|
|
149
|
+
if (slackRuntime.channelMappings.length > 0) {
|
|
150
|
+
console.log(`Mapped channels: ${slackRuntime.channelMappings.map((mapping) => mapping.channelName ?? mapping.channelId).join(", ")}`);
|
|
151
|
+
} else {
|
|
152
|
+
console.log("Monitored channels: all Slack channels/private groups where the bot is present");
|
|
153
|
+
}
|
|
154
|
+
console.log(`Escalation owner: ${config.user.name}`);
|
|
155
|
+
while (true) {
|
|
156
|
+
try {
|
|
157
|
+
const session = await openSocketModeConnection(slackRuntime.appToken, {
|
|
158
|
+
onEvent: (event) => {
|
|
159
|
+
void handleSlackEvent(event, deps).catch((err) => {
|
|
160
|
+
console.error("Slack event handling failed:", err);
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
onError: (err) => {
|
|
164
|
+
console.error("Slack Socket Mode error:", err.message);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
await session.closed;
|
|
168
|
+
console.warn(`Slack Socket Mode disconnected. Reconnecting in ${RECONNECT_DELAY_MS}ms...`);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error("Slack listener connection failed:", err);
|
|
171
|
+
}
|
|
172
|
+
await sleep(RECONNECT_DELAY_MS);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function handleSlackEvent(event, deps) {
|
|
176
|
+
if (!shouldHandleSlackEvent(event) || !event.channel || !event.ts) {
|
|
177
|
+
console.log(`[slack] Ignored unsupported event type=${event.type} subtype=${event.subtype ?? "none"}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const eventKey = `${event.channel}:${event.ts}`;
|
|
181
|
+
if (hasSeenRecentEvent(eventKey)) {
|
|
182
|
+
console.log(`[slack] Ignored duplicate event ${eventKey}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
markRecentEvent(eventKey);
|
|
186
|
+
const mapping = createRuntimeChannelMapping(event.channel, deps.slackRuntime.channelMappings, deps.config.user.name);
|
|
187
|
+
if (!isReplyCandidate(event, deps.config.user.assistantName)) {
|
|
188
|
+
console.log(`[slack] Ignored non-request in #${mapping.channelName ?? event.channel}: ${summarizeText(event.text)}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log(`[slack] Processing candidate in #${mapping.channelName ?? event.channel}: ${summarizeText(event.text)}`);
|
|
192
|
+
const connector = createSlackConnector();
|
|
193
|
+
await connector.authenticate(deps.slackIntegration);
|
|
194
|
+
const threadTs = event.thread_ts ?? event.ts;
|
|
195
|
+
const thread = await connector.fetchThread(event.channel, threadTs, mapping.channelName);
|
|
196
|
+
const threadMapping = {
|
|
197
|
+
...mapping,
|
|
198
|
+
channelName: thread.channelName ?? mapping.channelName
|
|
199
|
+
};
|
|
200
|
+
const resolution = await resolveProjectName(deps.storage, thread);
|
|
201
|
+
logProjectResolution(resolution);
|
|
202
|
+
const projectName = resolution.selectedProject;
|
|
203
|
+
if (!projectName) {
|
|
204
|
+
console.log("[slack] Could not resolve a project from the thread; falling back to abstain");
|
|
205
|
+
} else {
|
|
206
|
+
console.log(`[slack] Resolved project: ${projectName}`);
|
|
207
|
+
logSelectedProjectSummary(resolution, projectName);
|
|
208
|
+
}
|
|
209
|
+
await writeArtifact(
|
|
210
|
+
deps.storage,
|
|
211
|
+
`${WORKSPACE_PATHS.context.raw.slack}/${artifactKey(event.channel, threadTs)}.json`,
|
|
212
|
+
{
|
|
213
|
+
receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
214
|
+
event,
|
|
215
|
+
thread
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
const replyInput = await buildReplyInput(deps.config, deps.storage, threadMapping, thread, projectName);
|
|
219
|
+
logEvidenceSummary(replyInput.evidence);
|
|
220
|
+
await writeArtifact(
|
|
221
|
+
deps.storage,
|
|
222
|
+
`${WORKSPACE_PATHS.context.derived.slack.replyInputs}/${artifactKey(event.channel, threadTs)}.json`,
|
|
223
|
+
replyInput
|
|
224
|
+
);
|
|
225
|
+
const draft = await buildDraftOutput(deps.config, replyInput, threadMapping);
|
|
226
|
+
console.log(`[slack] Draft outcome for ${artifactKey(event.channel, threadTs)}: ${draft.outcome}`);
|
|
227
|
+
logDraftPreview(draft);
|
|
228
|
+
if (draft.citations.length > 0) {
|
|
229
|
+
console.log(`[slack] Draft citations: ${draft.citations.join(" | ")}`);
|
|
230
|
+
}
|
|
231
|
+
if (draft.missingFacts.length > 0) {
|
|
232
|
+
console.log(`[slack] Draft missing facts: ${draft.missingFacts.join(" | ")}`);
|
|
233
|
+
}
|
|
234
|
+
await writeArtifact(
|
|
235
|
+
deps.storage,
|
|
236
|
+
`${WORKSPACE_PATHS.context.derived.slack.drafts}/${artifactKey(event.channel, threadTs)}.json`,
|
|
237
|
+
draft
|
|
238
|
+
);
|
|
239
|
+
let delivery;
|
|
240
|
+
try {
|
|
241
|
+
delivery = await deliverDraft(connector, deps.slackRuntime, threadMapping, thread, draft, replyInput.projectName);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
244
|
+
console.error(`[slack] Delivery failed for ${artifactKey(event.channel, threadTs)}: ${message}`);
|
|
245
|
+
delivery = {
|
|
246
|
+
mode: deps.slackRuntime.reviewMode,
|
|
247
|
+
deliveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
248
|
+
channelId: thread.channelId,
|
|
249
|
+
threadTs: thread.threadTs,
|
|
250
|
+
reviewerUserId: threadMapping.reviewerUserId,
|
|
251
|
+
outcome: "delivery_error",
|
|
252
|
+
error: message
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
await writeArtifact(
|
|
256
|
+
deps.storage,
|
|
257
|
+
`${WORKSPACE_PATHS.context.derived.slack.deliveries}/${artifactKey(event.channel, threadTs)}.json`,
|
|
258
|
+
delivery
|
|
259
|
+
);
|
|
260
|
+
if (delivery.mode === "human_review") {
|
|
261
|
+
console.log(`[slack] Delivery mode: human_review (ephemeral draft only, visible to reviewer ${threadMapping.reviewerDisplayName ?? threadMapping.reviewerUserId})`);
|
|
262
|
+
console.log(`[slack] Reviewer target: ${threadMapping.reviewerDisplayName ?? "unknown"} (${threadMapping.reviewerUserId})`);
|
|
263
|
+
} else if (delivery.mode === "auto_send") {
|
|
264
|
+
console.log("[slack] Delivery mode: auto_send (public thread reply posted)");
|
|
265
|
+
console.log(`[slack] Public reply channel: #${threadMapping.channelName ?? thread.channelId} thread=${thread.threadTs}`);
|
|
266
|
+
}
|
|
267
|
+
console.log(`[slack] Delivery recorded for ${artifactKey(event.channel, threadTs)} (${String(delivery.outcome ?? "unknown")})`);
|
|
268
|
+
}
|
|
269
|
+
function shouldHandleSlackEvent(event) {
|
|
270
|
+
if (event.type !== "message" && event.type !== "app_mention") return false;
|
|
271
|
+
if (!event.channel || !event.ts) return false;
|
|
272
|
+
if (event.bot_id) return false;
|
|
273
|
+
if (event.type === "message" && event.subtype && event.subtype !== "thread_broadcast") return false;
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
function isReplyCandidate(event, assistantName) {
|
|
277
|
+
if (!shouldHandleSlackEvent(event)) return false;
|
|
278
|
+
if (event.type === "app_mention") return true;
|
|
279
|
+
const text = event.text?.trim();
|
|
280
|
+
if (!text) return false;
|
|
281
|
+
if (assistantName?.trim()) {
|
|
282
|
+
const assistantPattern = new RegExp(`\\b${escapeRegExp(assistantName.trim())}\\b`, "i");
|
|
283
|
+
if (assistantPattern.test(text)) return true;
|
|
284
|
+
}
|
|
285
|
+
return /[?]$/.test(text) || /\b(status|owner|blocked|blocker|eta|when|who|what|where|which|why|how|help|update|decision)\b/i.test(text) || /\b(tell|show|give)\s+me\b/i.test(text) || /\b(can|could|would|should)\s+you\b/i.test(text);
|
|
286
|
+
}
|
|
287
|
+
function resolveChannelMapping(channelId, mappings) {
|
|
288
|
+
return mappings.find((mapping) => mapping.channelId === channelId);
|
|
289
|
+
}
|
|
290
|
+
function createRuntimeChannelMapping(channelId, mappings, ownerName) {
|
|
291
|
+
return resolveChannelMapping(channelId, mappings) ?? {
|
|
292
|
+
channelId,
|
|
293
|
+
channelName: channelId,
|
|
294
|
+
reviewerUserId: "config-owner",
|
|
295
|
+
reviewerDisplayName: ownerName || "Configured user"
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function extractGitHubResources(text) {
|
|
299
|
+
const matches = [...text.matchAll(/github\.com\/([^/\s]+\/[^/\s]+)\/(issues|pull)\/(\d+)/g)];
|
|
300
|
+
return matches.map((match) => ({
|
|
301
|
+
repo: match[1],
|
|
302
|
+
kind: match[2],
|
|
303
|
+
number: match[3]
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
function extractGoogleDocIds(text) {
|
|
307
|
+
return [...text.matchAll(/docs\.google\.com\/document\/d\/([A-Za-z0-9_-]+)/g)].map((match) => match[1]);
|
|
308
|
+
}
|
|
309
|
+
async function buildReplyInput(config, storage, mapping, thread, projectName) {
|
|
310
|
+
const contextManager = createContextManager(storage);
|
|
311
|
+
const projectContext = await contextManager.getProjectContext(projectName ?? "");
|
|
312
|
+
const evidence = [];
|
|
313
|
+
for (const file of projectContext.canonicalFiles) {
|
|
314
|
+
evidence.push({
|
|
315
|
+
source: "project",
|
|
316
|
+
reference: file.path,
|
|
317
|
+
content: truncate(file.content, 2e3)
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
for (const file of projectContext.derivedFiles) {
|
|
321
|
+
evidence.push({
|
|
322
|
+
source: "project",
|
|
323
|
+
reference: file.path,
|
|
324
|
+
content: truncate(file.content, 1500)
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
for (const file of projectContext.rawFiles) {
|
|
328
|
+
evidence.push({
|
|
329
|
+
source: "raw",
|
|
330
|
+
reference: file.path,
|
|
331
|
+
content: truncate(file.content, 1500)
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
evidence.push(...await collectGitHubEvidence(config, mapping, thread));
|
|
335
|
+
evidence.push(...await collectGoogleDriveEvidence(config, mapping, thread));
|
|
336
|
+
return {
|
|
337
|
+
projectName: projectName ?? "",
|
|
338
|
+
thread,
|
|
339
|
+
evidence,
|
|
340
|
+
reviewMode: config.slackRuntime?.reviewMode ?? "auto_send",
|
|
341
|
+
provider: config.slackRuntime?.defaultProvider,
|
|
342
|
+
model: config.slackRuntime?.defaultModel
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
async function buildDraftOutput(config, replyInput, mapping) {
|
|
346
|
+
if (!replyInput.projectName) {
|
|
347
|
+
return {
|
|
348
|
+
outcome: "abstain",
|
|
349
|
+
draftText: "",
|
|
350
|
+
citations: [],
|
|
351
|
+
missingFacts: ["Could not determine which project the Slack thread is asking about."],
|
|
352
|
+
escalationOwner: mapping.reviewerDisplayName ?? mapping.reviewerUserId,
|
|
353
|
+
confidence: "low"
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const provider = config.slackRuntime?.defaultProvider;
|
|
357
|
+
const model = config.slackRuntime?.defaultModel;
|
|
358
|
+
if (!config.slackRuntime?.generationEnabled || !provider || !model) {
|
|
359
|
+
return {
|
|
360
|
+
outcome: "abstain",
|
|
361
|
+
draftText: "",
|
|
362
|
+
citations: replyInput.evidence.map((item) => item.reference).slice(0, 5),
|
|
363
|
+
missingFacts: ["Generation is disabled or provider/model is not configured."],
|
|
364
|
+
escalationOwner: mapping.reviewerDisplayName ?? mapping.reviewerUserId,
|
|
365
|
+
confidence: "low"
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
return await generateDraft(provider, model, config.providers, replyInput);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
return {
|
|
372
|
+
outcome: "abstain",
|
|
373
|
+
draftText: "",
|
|
374
|
+
citations: replyInput.evidence.map((item) => item.reference).slice(0, 5),
|
|
375
|
+
missingFacts: [err instanceof Error ? err.message : String(err)],
|
|
376
|
+
escalationOwner: mapping.reviewerDisplayName ?? mapping.reviewerUserId,
|
|
377
|
+
confidence: "low"
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function deliverDraft(connector, slackRuntime, mapping, thread, draft, projectName) {
|
|
382
|
+
if (slackRuntime.reviewMode === "auto_send") {
|
|
383
|
+
const threadReply = renderAutoSendMessage(draft, projectName);
|
|
384
|
+
const ts = await connector.postThreadReply(thread.channelId, thread.threadTs, threadReply);
|
|
385
|
+
return {
|
|
386
|
+
mode: "auto_send",
|
|
387
|
+
deliveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
388
|
+
channelId: thread.channelId,
|
|
389
|
+
threadTs: thread.threadTs,
|
|
390
|
+
messageTs: ts,
|
|
391
|
+
outcome: draft.outcome,
|
|
392
|
+
postedText: threadReply
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const reviewText = renderReviewMessage(mapping, draft, projectName);
|
|
396
|
+
await connector.postEphemeral(thread.channelId, mapping.reviewerUserId, reviewText, thread.threadTs);
|
|
397
|
+
return {
|
|
398
|
+
mode: "human_review",
|
|
399
|
+
deliveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
400
|
+
channelId: thread.channelId,
|
|
401
|
+
threadTs: thread.threadTs,
|
|
402
|
+
reviewerUserId: mapping.reviewerUserId,
|
|
403
|
+
outcome: draft.outcome
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async function collectGitHubEvidence(config, _mapping, thread) {
|
|
407
|
+
const githubIntegration = config.integrations.find((integration) => integration.type === "github" && integration.enabled);
|
|
408
|
+
const token = githubIntegration?.credentials?.token;
|
|
409
|
+
if (!token) {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
const linkedResources = extractGitHubResources(thread.messages.map((message) => message.text).join("\n")).slice(0, 3);
|
|
413
|
+
if (linkedResources.length === 0) {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
const evidence = [];
|
|
417
|
+
for (const resource of linkedResources) {
|
|
418
|
+
const endpoint = resource.kind === "pull" ? `https://api.github.com/repos/${resource.repo}/pulls/${resource.number}` : `https://api.github.com/repos/${resource.repo}/issues/${resource.number}`;
|
|
419
|
+
const response = await fetch(endpoint, {
|
|
420
|
+
headers: {
|
|
421
|
+
Authorization: `token ${token}`,
|
|
422
|
+
Accept: "application/vnd.github+json",
|
|
423
|
+
"User-Agent": "personal-assistant"
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
if (!response.ok) continue;
|
|
427
|
+
const data = await response.json();
|
|
428
|
+
evidence.push({
|
|
429
|
+
source: "github",
|
|
430
|
+
reference: data.html_url ?? `${resource.repo}#${resource.number}`,
|
|
431
|
+
content: truncate(`${data.title ?? "Untitled"}
|
|
432
|
+
State: ${data.state ?? "unknown"}
|
|
433
|
+
${data.body ?? ""}`, 1500)
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return evidence;
|
|
437
|
+
}
|
|
438
|
+
async function collectGoogleDriveEvidence(config, _mapping, thread) {
|
|
439
|
+
const gdriveIntegration = config.integrations.find((integration) => integration.type === "gdrive" && integration.enabled);
|
|
440
|
+
const credentials = gdriveIntegration?.credentials;
|
|
441
|
+
if (!credentials?.clientId || !credentials?.clientSecret || !credentials?.refreshToken) {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
const linkedDocIds = extractGoogleDocIds(thread.messages.map((message) => message.text).join("\n")).slice(0, 3);
|
|
445
|
+
if (linkedDocIds.length === 0) {
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
const oauth2Client = new google.auth.OAuth2(credentials.clientId, credentials.clientSecret);
|
|
449
|
+
oauth2Client.setCredentials({ refresh_token: credentials.refreshToken });
|
|
450
|
+
const drive = google.drive({ version: "v3", auth: oauth2Client });
|
|
451
|
+
const evidence = [];
|
|
452
|
+
for (const docId of linkedDocIds) {
|
|
453
|
+
try {
|
|
454
|
+
const meta = await drive.files.get({
|
|
455
|
+
fileId: docId,
|
|
456
|
+
fields: "name,webViewLink"
|
|
457
|
+
});
|
|
458
|
+
const exported = await drive.files.export({
|
|
459
|
+
fileId: docId,
|
|
460
|
+
mimeType: "text/plain"
|
|
461
|
+
});
|
|
462
|
+
evidence.push({
|
|
463
|
+
source: "gdrive",
|
|
464
|
+
reference: meta.data.webViewLink ?? docId,
|
|
465
|
+
content: truncate(`${meta.data.name ?? docId}
|
|
466
|
+
${String(exported.data)}`, 1500)
|
|
467
|
+
});
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return evidence;
|
|
472
|
+
}
|
|
473
|
+
function renderReviewMessage(mapping, draft, projectName) {
|
|
474
|
+
const lines = [
|
|
475
|
+
`*Atlas draft for ${projectName || "unknown project"}*`,
|
|
476
|
+
draft.outcome === "draft" ? draft.draftText || "_No draft text was returned._" : "_Atlas abstained because the available context was insufficient._"
|
|
477
|
+
];
|
|
478
|
+
if (draft.citations.length > 0) {
|
|
479
|
+
lines.push(`*Citations*
|
|
480
|
+
\u2022 ${draft.citations.join("\n\u2022 ")}`);
|
|
481
|
+
}
|
|
482
|
+
if (draft.missingFacts.length > 0) {
|
|
483
|
+
lines.push(`*Missing facts*
|
|
484
|
+
\u2022 ${draft.missingFacts.join("\n\u2022 ")}`);
|
|
485
|
+
}
|
|
486
|
+
if (draft.escalationOwner) {
|
|
487
|
+
lines.push(`*Escalation owner*
|
|
488
|
+
${draft.escalationOwner}`);
|
|
489
|
+
}
|
|
490
|
+
return truncate(lines.join("\n\n"), 3500);
|
|
491
|
+
}
|
|
492
|
+
function renderAutoSendMessage(draft, projectName) {
|
|
493
|
+
if (draft.outcome === "draft" && draft.draftText.trim()) {
|
|
494
|
+
return truncate(draft.draftText.trim(), 3500);
|
|
495
|
+
}
|
|
496
|
+
const lines = [
|
|
497
|
+
`I can't answer confidently from the current context${projectName ? ` for ${projectName}` : ""}.`
|
|
498
|
+
];
|
|
499
|
+
if (draft.missingFacts.length > 0) {
|
|
500
|
+
lines.push(`Missing context: ${draft.missingFacts.join("; ")}`);
|
|
501
|
+
}
|
|
502
|
+
if (draft.escalationOwner) {
|
|
503
|
+
lines.push(`Best escalation path: ${draft.escalationOwner}.`);
|
|
504
|
+
}
|
|
505
|
+
return truncate(lines.join("\n\n"), 3500);
|
|
506
|
+
}
|
|
507
|
+
function artifactKey(channelId, threadTs) {
|
|
508
|
+
return `${channelId}-${threadTs.replace(/[^\dA-Za-z_-]/g, "_")}`;
|
|
509
|
+
}
|
|
510
|
+
async function writeArtifact(storage, filePath, data) {
|
|
511
|
+
await storage.write(filePath, JSON.stringify(data, null, 2));
|
|
512
|
+
}
|
|
513
|
+
function truncate(value, maxLength) {
|
|
514
|
+
if (value.length <= maxLength) return value;
|
|
515
|
+
return `${value.slice(0, maxLength - 3)}...`;
|
|
516
|
+
}
|
|
517
|
+
function summarizeText(text, maxLength = 100) {
|
|
518
|
+
if (!text?.trim()) return "<no text>";
|
|
519
|
+
return truncate(text.replace(/\s+/g, " ").trim(), maxLength);
|
|
520
|
+
}
|
|
521
|
+
function escapeRegExp(value) {
|
|
522
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
523
|
+
}
|
|
524
|
+
async function resolveProjectName(storage, thread) {
|
|
525
|
+
const contextManager = createContextManager(storage);
|
|
526
|
+
const projectNames = await contextManager.listProjects().catch(() => []);
|
|
527
|
+
const threadText = thread.messages.map((message) => message.text).join(" ");
|
|
528
|
+
const sources = await loadProjectResolutionSources(storage, projectNames);
|
|
529
|
+
return inspectProjectResolution(sources, threadText);
|
|
530
|
+
}
|
|
531
|
+
function resolveProjectFromText(projectNames, text) {
|
|
532
|
+
const sources = projectNames.map((projectName) => ({
|
|
533
|
+
projectName,
|
|
534
|
+
latestSummary: "",
|
|
535
|
+
contextText: ""
|
|
536
|
+
}));
|
|
537
|
+
return inspectProjectResolution(sources, text).selectedProject;
|
|
538
|
+
}
|
|
539
|
+
function inspectProjectResolution(sources, text) {
|
|
540
|
+
const normalizedText = normalizeForMatching(text);
|
|
541
|
+
const queryTokens = uniqueTokens(tokenizeResolutionText(text));
|
|
542
|
+
if (sources.length === 0) {
|
|
543
|
+
return {
|
|
544
|
+
threadText: text,
|
|
545
|
+
normalizedText,
|
|
546
|
+
queryTokens,
|
|
547
|
+
candidates: [],
|
|
548
|
+
selectedProject: void 0
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (!normalizedText) {
|
|
552
|
+
return {
|
|
553
|
+
threadText: text,
|
|
554
|
+
normalizedText,
|
|
555
|
+
queryTokens,
|
|
556
|
+
candidates: sources.map((source) => ({
|
|
557
|
+
projectName: source.projectName,
|
|
558
|
+
normalizedProject: normalizeForMatching(source.projectName),
|
|
559
|
+
latestSummary: source.latestSummary,
|
|
560
|
+
matchedNameTokens: [],
|
|
561
|
+
matchedContextTokens: [],
|
|
562
|
+
matchedAliasTokens: [],
|
|
563
|
+
cosineSimilarity: 0,
|
|
564
|
+
coverageScore: 0,
|
|
565
|
+
aliasScore: 0,
|
|
566
|
+
phraseScore: 0,
|
|
567
|
+
score: 0
|
|
568
|
+
})),
|
|
569
|
+
selectedProject: void 0
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const profiles = sources.map((source) => createProjectResolutionProfile(source));
|
|
573
|
+
const idf = buildInverseDocumentFrequencies(profiles);
|
|
574
|
+
const candidates = profiles.map((profile) => scoreProjectMatch(profile, normalizedText, queryTokens, idf));
|
|
575
|
+
candidates.sort((a, b) => b.score - a.score || a.projectName.localeCompare(b.projectName));
|
|
576
|
+
if (sources.length === 1) {
|
|
577
|
+
return {
|
|
578
|
+
threadText: text,
|
|
579
|
+
normalizedText,
|
|
580
|
+
queryTokens,
|
|
581
|
+
candidates,
|
|
582
|
+
selectedProject: candidates[0]?.projectName
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const [best, second] = candidates;
|
|
586
|
+
const selectedProject = best && best.score >= PROJECT_RESOLUTION_MIN_SCORE && best.score - (second?.score ?? 0) >= PROJECT_RESOLUTION_MIN_MARGIN ? best.projectName : void 0;
|
|
587
|
+
return {
|
|
588
|
+
threadText: text,
|
|
589
|
+
normalizedText,
|
|
590
|
+
queryTokens,
|
|
591
|
+
candidates,
|
|
592
|
+
selectedProject
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function scoreProjectMatch(profile, haystack, queryTokens, idf) {
|
|
596
|
+
const { source, normalizedProject } = profile;
|
|
597
|
+
if (!normalizedProject) {
|
|
598
|
+
return {
|
|
599
|
+
projectName: source.projectName,
|
|
600
|
+
normalizedProject,
|
|
601
|
+
latestSummary: source.latestSummary,
|
|
602
|
+
matchedNameTokens: [],
|
|
603
|
+
matchedContextTokens: [],
|
|
604
|
+
matchedAliasTokens: [],
|
|
605
|
+
cosineSimilarity: 0,
|
|
606
|
+
coverageScore: 0,
|
|
607
|
+
aliasScore: 0,
|
|
608
|
+
phraseScore: 0,
|
|
609
|
+
score: 0
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const nameTokenSet = new Set(profile.nameTokens);
|
|
613
|
+
const contextTokenSet = /* @__PURE__ */ new Set([...profile.summaryTokens, ...profile.contextTokens]);
|
|
614
|
+
const aliasTokenSet = new Set(profile.aliasTokens);
|
|
615
|
+
const documentTokenSet = new Set(profile.documentTokens);
|
|
616
|
+
const queryTokenSet = new Set(queryTokens);
|
|
617
|
+
const fallbackIdf = Math.log((idf.size + 2) / 0.5) + 1;
|
|
618
|
+
const matchedNameTokens = queryTokens.filter((token) => nameTokenSet.has(token));
|
|
619
|
+
const matchedAliasTokens = queryTokens.filter((token) => aliasTokenSet.has(token));
|
|
620
|
+
const matchedContextTokens = queryTokens.filter((token) => contextTokenSet.has(token) && !nameTokenSet.has(token) && !aliasTokenSet.has(token));
|
|
621
|
+
const queryVector = buildWeightedTokenMap(queryTokens, idf, fallbackIdf, 1);
|
|
622
|
+
const documentVector = /* @__PURE__ */ new Map();
|
|
623
|
+
accumulateTokenWeights(documentVector, profile.nameTokens, idf, fallbackIdf, 3.5);
|
|
624
|
+
accumulateTokenWeights(documentVector, profile.summaryTokens, idf, fallbackIdf, 2.5);
|
|
625
|
+
accumulateTokenWeights(documentVector, profile.contextTokens, idf, fallbackIdf, 1.2);
|
|
626
|
+
accumulateTokenWeights(documentVector, profile.aliasTokens, idf, fallbackIdf, 3);
|
|
627
|
+
const totalQueryWeight = sumWeights(queryVector) || 1;
|
|
628
|
+
const matchedQueryWeight = [...queryTokenSet].filter((token) => documentTokenSet.has(token)).reduce((sum, token) => sum + (queryVector.get(token) ?? 0), 0);
|
|
629
|
+
const matchedAliasWeight = [...queryTokenSet].filter((token) => aliasTokenSet.has(token)).reduce((sum, token) => sum + (queryVector.get(token) ?? 0), 0);
|
|
630
|
+
const coverageScore = matchedQueryWeight / totalQueryWeight;
|
|
631
|
+
const aliasScore = matchedAliasWeight / totalQueryWeight;
|
|
632
|
+
const cosineSimilarity = computeCosineSimilarity(queryVector, documentVector);
|
|
633
|
+
const phraseScore = haystack.includes(normalizedProject) ? 1 : 0;
|
|
634
|
+
const score = phraseScore * 1.5 + cosineSimilarity * 1.2 + coverageScore * 1 + aliasScore * 0.8;
|
|
635
|
+
return {
|
|
636
|
+
projectName: source.projectName,
|
|
637
|
+
normalizedProject,
|
|
638
|
+
latestSummary: source.latestSummary,
|
|
639
|
+
matchedNameTokens,
|
|
640
|
+
matchedContextTokens,
|
|
641
|
+
matchedAliasTokens,
|
|
642
|
+
cosineSimilarity,
|
|
643
|
+
coverageScore,
|
|
644
|
+
aliasScore,
|
|
645
|
+
phraseScore,
|
|
646
|
+
score
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function logProjectResolution(debug) {
|
|
650
|
+
console.log("[slack] Project resolution strategy: weighted TF-IDF similarity over project name, latest project summary, and recent project context, with stemming, stopword removal, and acronym alias extraction; require unique best score >= 0.45 with margin >= 0.08 or abstain");
|
|
651
|
+
console.log(`[slack] Registered projects: ${debug.candidates.length > 0 ? debug.candidates.map((candidate) => candidate.projectName).join(", ") : "none"}`);
|
|
652
|
+
console.log(`[slack] Resolution text: ${summarizeText(debug.threadText, 180)}`);
|
|
653
|
+
console.log(`[slack] Normalized text: ${debug.normalizedText || "<empty>"}`);
|
|
654
|
+
console.log(`[slack] Query tokens: ${debug.queryTokens.length > 0 ? debug.queryTokens.join(", ") : "none"}`);
|
|
655
|
+
if (debug.candidates.length === 0) {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
console.log(
|
|
659
|
+
`[slack] Project candidate scores: ${debug.candidates.map((candidate) => `${candidate.projectName}=total:${formatScore(candidate.score)},cosine:${formatScore(candidate.cosineSimilarity)},coverage:${formatScore(candidate.coverageScore)},alias:${formatScore(candidate.aliasScore)},phrase:${formatScore(candidate.phraseScore)}${candidate.matchedNameTokens.length > 0 ? `,name:[${candidate.matchedNameTokens.join(", ")}]` : ""}${candidate.matchedContextTokens.length > 0 ? `,context:[${candidate.matchedContextTokens.join(", ")}]` : ""}${candidate.matchedAliasTokens.length > 0 ? `,aliases:[${candidate.matchedAliasTokens.join(", ")}]` : ""},summary:${JSON.stringify(summarizeText(candidate.latestSummary, 100))}`).join(" | ")}`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
function logSelectedProjectSummary(debug, projectName) {
|
|
663
|
+
const selected = debug.candidates.find((candidate) => candidate.projectName === projectName);
|
|
664
|
+
if (!selected?.latestSummary) {
|
|
665
|
+
console.log("[slack] Selected project summary: <empty>");
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
console.log("[slack] Selected project summary begin");
|
|
669
|
+
for (const line of selected.latestSummary.split("\n")) {
|
|
670
|
+
console.log(`[slack] > ${line}`);
|
|
671
|
+
}
|
|
672
|
+
console.log("[slack] Selected project summary end");
|
|
673
|
+
}
|
|
674
|
+
function logEvidenceSummary(evidence) {
|
|
675
|
+
const counts = /* @__PURE__ */ new Map();
|
|
676
|
+
for (const item of evidence) {
|
|
677
|
+
counts.set(item.source, (counts.get(item.source) ?? 0) + 1);
|
|
678
|
+
}
|
|
679
|
+
console.log(`[slack] Evidence counts: ${[...counts.entries()].map(([source, count]) => `${source}=${count}`).join(" | ") || "none"}`);
|
|
680
|
+
const rawRefs = evidence.filter((item) => item.source === "raw").map((item) => item.reference);
|
|
681
|
+
if (rawRefs.length > 0) {
|
|
682
|
+
console.log(`[slack] Raw context references: ${rawRefs.join(" | ")}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function logDraftPreview(draft) {
|
|
686
|
+
if (!draft.draftText?.trim()) {
|
|
687
|
+
console.log("[slack] Draft text: <empty>");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
console.log("[slack] Draft text begin");
|
|
691
|
+
for (const line of draft.draftText.trim().split("\n")) {
|
|
692
|
+
console.log(`[slack] | ${line}`);
|
|
693
|
+
}
|
|
694
|
+
console.log("[slack] Draft text end");
|
|
695
|
+
}
|
|
696
|
+
function normalizeForMatching(value) {
|
|
697
|
+
return value.replace(/<@[A-Z0-9]+>/gi, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase().replace(/[^a-z0-9\s]+/g, " ").replace(/\s+/g, " ").trim();
|
|
698
|
+
}
|
|
699
|
+
async function loadProjectResolutionSources(storage, projectNames) {
|
|
700
|
+
return Promise.all(projectNames.map(async (projectName) => {
|
|
701
|
+
const filePath = `${WORKSPACE_PATHS.context.canonical.projects}/${projectName}.md`;
|
|
702
|
+
let content = "";
|
|
703
|
+
try {
|
|
704
|
+
content = await storage.read(filePath);
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
projectName,
|
|
709
|
+
latestSummary: extractLatestProjectSummary(content),
|
|
710
|
+
contextText: extractRecentProjectContext(content)
|
|
711
|
+
};
|
|
712
|
+
}));
|
|
713
|
+
}
|
|
714
|
+
function extractLatestProjectSummary(content) {
|
|
715
|
+
const normalized = content.trim();
|
|
716
|
+
if (!normalized) return "";
|
|
717
|
+
const noteSections = normalized.split(/\n## Note - [^\n]+\n+/).map((section) => section.trim()).filter(Boolean);
|
|
718
|
+
if (noteSections.length > 0) {
|
|
719
|
+
return truncate(noteSections[noteSections.length - 1], 800);
|
|
720
|
+
}
|
|
721
|
+
return truncate(normalized, 800);
|
|
722
|
+
}
|
|
723
|
+
function extractRecentProjectContext(content) {
|
|
724
|
+
const normalized = content.trim();
|
|
725
|
+
if (!normalized) return "";
|
|
726
|
+
return truncate(normalized.slice(-2500), 2500);
|
|
727
|
+
}
|
|
728
|
+
function tokenizeResolutionText(value) {
|
|
729
|
+
if (!value) return [];
|
|
730
|
+
return normalizeForMatching(value).split(" ").map((token) => stemResolutionToken(token.trim())).filter((token) => token.length > 2 && !RESOLUTION_STOPWORDS.has(token));
|
|
731
|
+
}
|
|
732
|
+
function stemResolutionToken(token) {
|
|
733
|
+
if (!token) return "";
|
|
734
|
+
let stemmed = token.toLowerCase();
|
|
735
|
+
if (stemmed.endsWith("'s")) {
|
|
736
|
+
stemmed = stemmed.slice(0, -2);
|
|
737
|
+
}
|
|
738
|
+
if (stemmed.length > 4 && stemmed.endsWith("ies")) {
|
|
739
|
+
return `${stemmed.slice(0, -3)}y`;
|
|
740
|
+
}
|
|
741
|
+
if (stemmed.length > 5 && stemmed.endsWith("ing")) {
|
|
742
|
+
stemmed = stemmed.slice(0, -3);
|
|
743
|
+
} else if (stemmed.length > 4 && stemmed.endsWith("ed")) {
|
|
744
|
+
stemmed = stemmed.slice(0, -2);
|
|
745
|
+
}
|
|
746
|
+
if (stemmed.length > 4 && stemmed.endsWith("es")) {
|
|
747
|
+
stemmed = stemmed.slice(0, -2);
|
|
748
|
+
} else if (stemmed.length > 3 && stemmed.endsWith("s") && !stemmed.endsWith("ss")) {
|
|
749
|
+
stemmed = stemmed.slice(0, -1);
|
|
750
|
+
}
|
|
751
|
+
return stemmed;
|
|
752
|
+
}
|
|
753
|
+
function createProjectResolutionProfile(source) {
|
|
754
|
+
const nameTokens = tokenizeResolutionText(source.projectName);
|
|
755
|
+
const summaryTokens = tokenizeResolutionText(source.latestSummary);
|
|
756
|
+
const contextTokens = tokenizeResolutionText(source.contextText);
|
|
757
|
+
const aliasTokens = uniqueTokens([
|
|
758
|
+
...extractAcronymTokens(source.projectName),
|
|
759
|
+
...extractAcronymTokens(source.latestSummary),
|
|
760
|
+
...extractAcronymTokens(source.contextText)
|
|
761
|
+
]);
|
|
762
|
+
return {
|
|
763
|
+
source,
|
|
764
|
+
normalizedProject: normalizeForMatching(source.projectName),
|
|
765
|
+
nameTokens,
|
|
766
|
+
summaryTokens,
|
|
767
|
+
contextTokens,
|
|
768
|
+
aliasTokens,
|
|
769
|
+
documentTokens: uniqueTokens([...nameTokens, ...summaryTokens, ...contextTokens, ...aliasTokens])
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
function extractAcronymTokens(value) {
|
|
773
|
+
if (!value) return [];
|
|
774
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
775
|
+
for (const match of value.matchAll(/\b(?:[A-Z][a-z0-9]+){2,}\b/g)) {
|
|
776
|
+
const acronym = (match[0].match(/[A-Z]/g) ?? []).join("").toLowerCase();
|
|
777
|
+
if (acronym.length >= 2) {
|
|
778
|
+
aliases.add(acronym);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
for (const match of value.matchAll(/\b[A-Z][A-Za-z0-9]+(?:[\s_-]+[A-Z][A-Za-z0-9]+){1,4}\b/g)) {
|
|
782
|
+
const words = match[0].split(/[\s_-]+/).filter(Boolean);
|
|
783
|
+
const acronym = words.map((word) => word[0]?.toLowerCase() ?? "").join("");
|
|
784
|
+
if (acronym.length >= 2) {
|
|
785
|
+
aliases.add(acronym);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const separatorParts = value.split(/[\s_-]+/).map((part) => part.replace(/[^A-Za-z0-9]/g, "")).filter(Boolean);
|
|
789
|
+
if (separatorParts.length >= 2 && separatorParts.length <= 5) {
|
|
790
|
+
const acronym = separatorParts.map((part) => part[0]?.toLowerCase() ?? "").join("");
|
|
791
|
+
if (acronym.length >= 2) {
|
|
792
|
+
aliases.add(acronym);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return [...aliases].map((token) => stemResolutionToken(token)).filter((token) => token.length > 1);
|
|
796
|
+
}
|
|
797
|
+
function uniqueTokens(tokens) {
|
|
798
|
+
return [...new Set(tokens.filter(Boolean))];
|
|
799
|
+
}
|
|
800
|
+
function buildInverseDocumentFrequencies(profiles) {
|
|
801
|
+
const frequencies = /* @__PURE__ */ new Map();
|
|
802
|
+
for (const profile of profiles) {
|
|
803
|
+
for (const token of new Set(profile.documentTokens)) {
|
|
804
|
+
frequencies.set(token, (frequencies.get(token) ?? 0) + 1);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const totalDocuments = Math.max(profiles.length, 1);
|
|
808
|
+
const idf = /* @__PURE__ */ new Map();
|
|
809
|
+
for (const [token, frequency] of frequencies.entries()) {
|
|
810
|
+
idf.set(token, Math.log((totalDocuments + 1) / (frequency + 0.5)) + 1);
|
|
811
|
+
}
|
|
812
|
+
return idf;
|
|
813
|
+
}
|
|
814
|
+
function buildWeightedTokenMap(tokens, idf, fallbackIdf, multiplier) {
|
|
815
|
+
const weights = /* @__PURE__ */ new Map();
|
|
816
|
+
accumulateTokenWeights(weights, tokens, idf, fallbackIdf, multiplier);
|
|
817
|
+
return weights;
|
|
818
|
+
}
|
|
819
|
+
function accumulateTokenWeights(weights, tokens, idf, fallbackIdf, multiplier) {
|
|
820
|
+
for (const token of tokens) {
|
|
821
|
+
const idfWeight = idf.get(token) ?? fallbackIdf;
|
|
822
|
+
weights.set(token, (weights.get(token) ?? 0) + idfWeight * multiplier);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function sumWeights(weights) {
|
|
826
|
+
let total = 0;
|
|
827
|
+
for (const value of weights.values()) {
|
|
828
|
+
total += value;
|
|
829
|
+
}
|
|
830
|
+
return total;
|
|
831
|
+
}
|
|
832
|
+
function computeCosineSimilarity(left, right) {
|
|
833
|
+
let dot = 0;
|
|
834
|
+
let leftNorm = 0;
|
|
835
|
+
let rightNorm = 0;
|
|
836
|
+
for (const value of left.values()) {
|
|
837
|
+
leftNorm += value * value;
|
|
838
|
+
}
|
|
839
|
+
for (const value of right.values()) {
|
|
840
|
+
rightNorm += value * value;
|
|
841
|
+
}
|
|
842
|
+
for (const [token, leftValue] of left.entries()) {
|
|
843
|
+
dot += leftValue * (right.get(token) ?? 0);
|
|
844
|
+
}
|
|
845
|
+
if (leftNorm === 0 || rightNorm === 0) {
|
|
846
|
+
return 0;
|
|
847
|
+
}
|
|
848
|
+
return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
|
|
849
|
+
}
|
|
850
|
+
function formatScore(value) {
|
|
851
|
+
return value.toFixed(3);
|
|
852
|
+
}
|
|
853
|
+
function hasSeenRecentEvent(eventKey) {
|
|
854
|
+
const seenAt = recentEventKeys.get(eventKey);
|
|
855
|
+
if (!seenAt) return false;
|
|
856
|
+
if (Date.now() - seenAt > RECENT_EVENT_TTL_MS) {
|
|
857
|
+
recentEventKeys.delete(eventKey);
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
function markRecentEvent(eventKey) {
|
|
863
|
+
pruneRecentEvents();
|
|
864
|
+
recentEventKeys.set(eventKey, Date.now());
|
|
865
|
+
}
|
|
866
|
+
function pruneRecentEvents() {
|
|
867
|
+
const now = Date.now();
|
|
868
|
+
for (const [eventKey, seenAt] of recentEventKeys.entries()) {
|
|
869
|
+
if (now - seenAt > RECENT_EVENT_TTL_MS) {
|
|
870
|
+
recentEventKeys.delete(eventKey);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function sleep(ms) {
|
|
875
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
876
|
+
}
|
|
877
|
+
export {
|
|
878
|
+
extractGitHubResources,
|
|
879
|
+
extractGoogleDocIds,
|
|
880
|
+
handleSlackEvent,
|
|
881
|
+
inspectProjectResolution,
|
|
882
|
+
isReplyCandidate,
|
|
883
|
+
resolveChannelMapping,
|
|
884
|
+
resolveProjectFromText,
|
|
885
|
+
shouldHandleSlackEvent,
|
|
886
|
+
startSlackListener
|
|
887
|
+
};
|
|
888
|
+
//# sourceMappingURL=slack-listener.js.map
|