@agent-native/core 0.111.4 → 0.112.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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +3 -1
- package/corpus/core/src/cli/code-agent-executor.ts +17 -5
- package/corpus/core/src/cli/code-agent-runs.ts +43 -2
- package/corpus/core/src/cli/multi-frontier-runs.ts +841 -0
- package/corpus/core/src/coding-tools/index.ts +27 -8
- package/corpus/core/src/embeddings/index.ts +233 -0
- package/corpus/core/src/index.ts +2 -0
- package/corpus/core/src/search/index.ts +413 -0
- package/corpus/templates/brain/.agents/skills/brain/RUNBOOK.md +48 -5
- package/corpus/templates/brain/.agents/skills/brain/SKILL.md +46 -14
- package/corpus/templates/brain/README.md +34 -3
- package/corpus/templates/brain/actions/approve-proposal.ts +2 -0
- package/corpus/templates/brain/actions/claim-distillation.ts +24 -27
- package/corpus/templates/brain/actions/create-source.ts +1 -1
- package/corpus/templates/brain/actions/get-knowledge.ts +6 -2
- package/corpus/templates/brain/actions/get-pilot-report.ts +44 -8
- package/corpus/templates/brain/actions/import-capture.ts +21 -9
- package/corpus/templates/brain/actions/import-transcript.ts +42 -14
- package/corpus/templates/brain/actions/list-captures.ts +10 -1
- package/corpus/templates/brain/actions/list-distillation-queue.ts +39 -14
- package/corpus/templates/brain/actions/list-projects.ts +51 -0
- package/corpus/templates/brain/actions/list-proposals.ts +80 -4
- package/corpus/templates/brain/actions/list-sources.ts +17 -3
- package/corpus/templates/brain/actions/manage-project.ts +118 -0
- package/corpus/templates/brain/actions/mark-capture-distilled.ts +68 -20
- package/corpus/templates/brain/actions/navigate.ts +1 -0
- package/corpus/templates/brain/actions/reject-proposal.ts +6 -1
- package/corpus/templates/brain/actions/resanitize-captures.ts +159 -33
- package/corpus/templates/brain/actions/review-proposal.ts +2 -0
- package/corpus/templates/brain/actions/search-everything.ts +10 -1
- package/corpus/templates/brain/actions/search-knowledge.ts +1 -1
- package/corpus/templates/brain/actions/set-settings.ts +36 -1
- package/corpus/templates/brain/actions/update-proposal.ts +2 -0
- package/corpus/templates/brain/app/hooks/use-distillation-bridge.ts +18 -9
- package/corpus/templates/brain/app/hooks/use-navigation-state.ts +3 -0
- package/corpus/templates/brain/app/i18n-data.ts +731 -2
- package/corpus/templates/brain/app/lib/brain.ts +33 -1
- package/corpus/templates/brain/app/routes/ops.tsx +47 -0
- package/corpus/templates/brain/app/routes/review.tsx +41 -2
- package/corpus/templates/brain/app/routes/search.tsx +33 -2
- package/corpus/templates/brain/app/routes/settings.tsx +158 -0
- package/corpus/templates/brain/app/routes/sources.tsx +31 -0
- package/corpus/templates/brain/changelog/2026-07-19-added-private-source-safe-semantic-search.md +6 -0
- package/corpus/templates/brain/jobs/process-ingest-queue.ts +270 -36
- package/corpus/templates/brain/package.json +1 -0
- package/corpus/templates/brain/server/db/index.ts +18 -0
- package/corpus/templates/brain/server/db/schema.ts +197 -1
- package/corpus/templates/brain/server/jobs/distillation-queue.ts +12 -5
- package/corpus/templates/brain/server/lib/audiences.ts +708 -0
- package/corpus/templates/brain/server/lib/brain-health.ts +251 -38
- package/corpus/templates/brain/server/lib/brain.ts +877 -50
- package/corpus/templates/brain/server/lib/capture-sanitization.ts +217 -53
- package/corpus/templates/brain/server/lib/connectors.ts +556 -70
- package/corpus/templates/brain/server/lib/demo.ts +17 -4
- package/corpus/templates/brain/server/lib/hybrid-search.ts +363 -0
- package/corpus/templates/brain/server/lib/ingest-queue.ts +104 -0
- package/corpus/templates/brain/server/lib/privacy-readiness.ts +24 -0
- package/corpus/templates/brain/server/lib/search-index-contracts.ts +72 -0
- package/corpus/templates/brain/server/lib/search-index.ts +714 -0
- package/corpus/templates/brain/server/lib/search.ts +120 -4
- package/corpus/templates/brain/server/lib/sensitivity-policy.ts +174 -0
- package/corpus/templates/brain/server/lib/slack-events.ts +281 -0
- package/corpus/templates/brain/server/onboarding.ts +29 -0
- package/corpus/templates/brain/server/plugins/db.ts +204 -0
- package/corpus/templates/brain/server/register-secrets.ts +46 -0
- package/corpus/templates/brain/server/routes/api/_agent-native/brain/ingest.post.ts +36 -16
- package/corpus/templates/brain/server/routes/api/_agent-native/brain/slack-events.post.ts +28 -0
- package/corpus/templates/brain/shared/types.ts +10 -0
- package/dist/cli/code-agent-executor.d.ts +8 -9
- package/dist/cli/code-agent-executor.d.ts.map +1 -1
- package/dist/cli/code-agent-executor.js +9 -3
- package/dist/cli/code-agent-executor.js.map +1 -1
- package/dist/cli/code-agent-runs.d.ts +5 -0
- package/dist/cli/code-agent-runs.d.ts.map +1 -1
- package/dist/cli/code-agent-runs.js +33 -2
- package/dist/cli/code-agent-runs.js.map +1 -1
- package/dist/cli/multi-frontier-runs.d.ts +131 -0
- package/dist/cli/multi-frontier-runs.d.ts.map +1 -0
- package/dist/cli/multi-frontier-runs.js +529 -0
- package/dist/cli/multi-frontier-runs.js.map +1 -0
- package/dist/coding-tools/index.d.ts +1 -0
- package/dist/coding-tools/index.d.ts.map +1 -1
- package/dist/coding-tools/index.js +25 -7
- package/dist/coding-tools/index.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/embeddings/index.d.ts +24 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +172 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/search/index.d.ts +88 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +267 -0
- package/dist/search/index.js.map +1 -0
- package/package.json +3 -1
|
@@ -478,7 +478,7 @@ const retrievalEvalCases: RetrievalEvalCase[] = [
|
|
|
478
478
|
"For a stale Fusion branch, should the agent checkout, reset, stash, or repair the branch automatically?",
|
|
479
479
|
requiredTerms: [
|
|
480
480
|
"fusion",
|
|
481
|
-
["do not", "don't", "never"],
|
|
481
|
+
["do not", "don't", "never", "avoid"],
|
|
482
482
|
"git checkout",
|
|
483
483
|
"reset",
|
|
484
484
|
],
|
|
@@ -568,7 +568,6 @@ const retrievalEvalCases: RetrievalEvalCase[] = [
|
|
|
568
568
|
label: "PII is redacted from retrieval output",
|
|
569
569
|
question:
|
|
570
570
|
"What does the #dev-fusion privacy redaction output say about durable escalation rotation?",
|
|
571
|
-
expectedTitle: "#dev-fusion privacy redaction output",
|
|
572
571
|
requiredTerms: [
|
|
573
572
|
"durable escalation rotation",
|
|
574
573
|
"[redacted]",
|
|
@@ -694,13 +693,27 @@ function evidence(
|
|
|
694
693
|
const metadata = parseJson<Record<string, unknown>>(capture.metadataJson, {});
|
|
695
694
|
return {
|
|
696
695
|
captureId: capture.id,
|
|
697
|
-
quote,
|
|
696
|
+
quote: exactPersistedEvidenceQuote(capture.content, quote),
|
|
698
697
|
note,
|
|
699
698
|
sourceUrl:
|
|
700
699
|
typeof metadata.sourceUrl === "string" ? metadata.sourceUrl : undefined,
|
|
701
700
|
};
|
|
702
701
|
}
|
|
703
702
|
|
|
703
|
+
function exactPersistedEvidenceQuote(content: string, preferred: string) {
|
|
704
|
+
const exactPreferred = preferred.trim();
|
|
705
|
+
if (exactPreferred && content.includes(exactPreferred)) return exactPreferred;
|
|
706
|
+
|
|
707
|
+
const longestLine = content
|
|
708
|
+
.split(/\r?\n/)
|
|
709
|
+
.map((line) => line.trim())
|
|
710
|
+
.filter(Boolean)
|
|
711
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
712
|
+
if (longestLine) return longestLine;
|
|
713
|
+
|
|
714
|
+
return content.slice(0, 280).trim();
|
|
715
|
+
}
|
|
716
|
+
|
|
704
717
|
function serializeSeedCapture(
|
|
705
718
|
row: typeof schema.brainRawCaptures.$inferSelect,
|
|
706
719
|
) {
|
|
@@ -1615,7 +1628,7 @@ export async function runBrainDemoEval(
|
|
|
1615
1628
|
"pii-redaction",
|
|
1616
1629
|
"PII is redacted before queryable storage",
|
|
1617
1630
|
!!redacted &&
|
|
1618
|
-
|
|
1631
|
+
["published", "redacted"].includes(redacted.status) &&
|
|
1619
1632
|
!redactedText.includes("ava.cho@example.com") &&
|
|
1620
1633
|
!redactedText.includes("+1 415 555 1212"),
|
|
1621
1634
|
redacted
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { getDbExec, isPostgres } from "@agent-native/core/db";
|
|
2
|
+
import {
|
|
3
|
+
availableEmbeddingFamilies,
|
|
4
|
+
defaultEmbeddingFamily,
|
|
5
|
+
} from "@agent-native/core/embeddings";
|
|
6
|
+
import {
|
|
7
|
+
queryPgVectorIndex,
|
|
8
|
+
queryPostgresFts,
|
|
9
|
+
} from "@agent-native/core/search";
|
|
10
|
+
import { normalizeSearchTerms } from "@agent-native/core/search-utils";
|
|
11
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
12
|
+
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
BrainCaptureKind,
|
|
16
|
+
BrainSourceProvider,
|
|
17
|
+
} from "../../shared/types.js";
|
|
18
|
+
import { getDb, schema } from "../db/index.js";
|
|
19
|
+
import { listAccessibleAudienceIds } from "./audiences.js";
|
|
20
|
+
import { BRAIN_SEARCH_INDEX_VERSION } from "./search-index-contracts.js";
|
|
21
|
+
|
|
22
|
+
export interface HybridCandidate {
|
|
23
|
+
id: string;
|
|
24
|
+
artifactId: string;
|
|
25
|
+
captureId: string;
|
|
26
|
+
sourceId: string;
|
|
27
|
+
audienceId: string;
|
|
28
|
+
title: string;
|
|
29
|
+
text: string;
|
|
30
|
+
capturedAt: string;
|
|
31
|
+
lexicalRank?: number;
|
|
32
|
+
semanticRank?: number;
|
|
33
|
+
authority?: number;
|
|
34
|
+
freshness?: number;
|
|
35
|
+
lane: "lexical" | "semantic" | "hybrid";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface HybridSearchResult extends HybridCandidate {
|
|
39
|
+
score: number;
|
|
40
|
+
reasons: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const RRF_K = 60;
|
|
44
|
+
const SEARCH_NAMESPACE = "brain";
|
|
45
|
+
|
|
46
|
+
export function reciprocalRankFusion(
|
|
47
|
+
candidates: HybridCandidate[],
|
|
48
|
+
k = RRF_K,
|
|
49
|
+
): HybridSearchResult[] {
|
|
50
|
+
return candidates
|
|
51
|
+
.map((candidate) => {
|
|
52
|
+
const lexical = candidate.lexicalRank
|
|
53
|
+
? 1 / (k + candidate.lexicalRank)
|
|
54
|
+
: 0;
|
|
55
|
+
const semantic = candidate.semanticRank
|
|
56
|
+
? 1 / (k + candidate.semanticRank)
|
|
57
|
+
: 0;
|
|
58
|
+
const authority = candidate.authority ?? 0;
|
|
59
|
+
const freshness = candidate.freshness ?? 0;
|
|
60
|
+
const reasons = [
|
|
61
|
+
...(candidate.lexicalRank ? ["lexical-match"] : []),
|
|
62
|
+
...(candidate.semanticRank ? ["semantic-match"] : []),
|
|
63
|
+
...(authority > 0 ? ["authority"] : []),
|
|
64
|
+
...(freshness > 0 ? ["fresh"] : []),
|
|
65
|
+
];
|
|
66
|
+
return {
|
|
67
|
+
...candidate,
|
|
68
|
+
score: lexical + semantic + authority * 0.01 + freshness * 0.005,
|
|
69
|
+
reasons,
|
|
70
|
+
};
|
|
71
|
+
})
|
|
72
|
+
.sort(
|
|
73
|
+
(a, b) => b.score - a.score || b.capturedAt.localeCompare(a.capturedAt),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function incrementalIdf(
|
|
78
|
+
term: string,
|
|
79
|
+
documents: readonly string[],
|
|
80
|
+
): number {
|
|
81
|
+
const normalized = term.toLowerCase();
|
|
82
|
+
const count = documents.filter((document) =>
|
|
83
|
+
document.toLowerCase().includes(normalized),
|
|
84
|
+
).length;
|
|
85
|
+
return Math.log((documents.length + 1) / (count + 1)) + 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function lexicalScore(
|
|
89
|
+
text: string,
|
|
90
|
+
terms: string[],
|
|
91
|
+
corpus: readonly string[],
|
|
92
|
+
): number {
|
|
93
|
+
const lower = text.toLowerCase();
|
|
94
|
+
return terms.reduce(
|
|
95
|
+
(score, term) =>
|
|
96
|
+
score + (lower.includes(term) ? incrementalIdf(term, corpus) : 0),
|
|
97
|
+
0,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Audience membership is the first database predicate for every index lane. */
|
|
102
|
+
export async function hybridSearchArtifacts(input: {
|
|
103
|
+
query: string;
|
|
104
|
+
provider?: string;
|
|
105
|
+
kind?: string;
|
|
106
|
+
projectId?: string;
|
|
107
|
+
limit?: number;
|
|
108
|
+
rerank?: boolean;
|
|
109
|
+
}): Promise<HybridSearchResult[]> {
|
|
110
|
+
const terms = normalizeSearchTerms(input.query);
|
|
111
|
+
if (!terms.length) return [];
|
|
112
|
+
const db = getDb();
|
|
113
|
+
const projectSourceIds = input.projectId
|
|
114
|
+
? (
|
|
115
|
+
await db
|
|
116
|
+
.select({ sourceId: schema.brainProjectSources.sourceId })
|
|
117
|
+
.from(schema.brainProjectSources)
|
|
118
|
+
.innerJoin(
|
|
119
|
+
schema.brainProjects,
|
|
120
|
+
eq(schema.brainProjects.id, schema.brainProjectSources.projectId),
|
|
121
|
+
)
|
|
122
|
+
.where(
|
|
123
|
+
and(
|
|
124
|
+
eq(schema.brainProjectSources.projectId, input.projectId),
|
|
125
|
+
accessFilter(schema.brainProjects, schema.brainProjectShares),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
).map((row) => row.sourceId)
|
|
129
|
+
: undefined;
|
|
130
|
+
if (projectSourceIds && !projectSourceIds.length) return [];
|
|
131
|
+
const audienceIds = await listAccessibleAudienceIds(projectSourceIds);
|
|
132
|
+
if (!audienceIds.length) return [];
|
|
133
|
+
const baseFilter = and(
|
|
134
|
+
eq(schema.brainSearchArtifacts.status, "active"),
|
|
135
|
+
eq(schema.brainRawCaptures.sensitivityDisposition, "allowed"),
|
|
136
|
+
eq(
|
|
137
|
+
schema.brainSearchArtifacts.contentHash,
|
|
138
|
+
schema.brainRawCaptures.contentHash,
|
|
139
|
+
),
|
|
140
|
+
eq(
|
|
141
|
+
schema.brainSearchArtifacts.aclHash,
|
|
142
|
+
schema.brainRawCaptures.audienceAclHash,
|
|
143
|
+
),
|
|
144
|
+
eq(
|
|
145
|
+
schema.brainSearchArtifacts.sensitivityPolicyVersion,
|
|
146
|
+
schema.brainRawCaptures.sensitivityPolicyVersion,
|
|
147
|
+
),
|
|
148
|
+
eq(schema.brainSearchArtifacts.indexVersion, BRAIN_SEARCH_INDEX_VERSION),
|
|
149
|
+
inArray(schema.brainSearchArtifacts.audienceId, audienceIds),
|
|
150
|
+
accessFilter(schema.brainSources, schema.brainSourceShares),
|
|
151
|
+
projectSourceIds
|
|
152
|
+
? inArray(schema.brainSearchArtifacts.sourceId, projectSourceIds)
|
|
153
|
+
: undefined,
|
|
154
|
+
input.provider
|
|
155
|
+
? eq(schema.brainSources.provider, input.provider as BrainSourceProvider)
|
|
156
|
+
: undefined,
|
|
157
|
+
input.kind
|
|
158
|
+
? eq(schema.brainRawCaptures.kind, input.kind as BrainCaptureKind)
|
|
159
|
+
: undefined,
|
|
160
|
+
);
|
|
161
|
+
const selectArtifacts = (artifactIds?: string[]) =>
|
|
162
|
+
db
|
|
163
|
+
.select({
|
|
164
|
+
id: schema.brainSearchArtifacts.id,
|
|
165
|
+
captureId: schema.brainSearchArtifacts.captureId,
|
|
166
|
+
sourceId: schema.brainSearchArtifacts.sourceId,
|
|
167
|
+
audienceId: schema.brainSearchArtifacts.audienceId,
|
|
168
|
+
title: schema.brainSearchArtifacts.title,
|
|
169
|
+
question: schema.brainSearchArtifacts.question,
|
|
170
|
+
summary: schema.brainSearchArtifacts.summary,
|
|
171
|
+
resolution: schema.brainSearchArtifacts.resolution,
|
|
172
|
+
capturedAt: schema.brainSearchArtifacts.capturedAt,
|
|
173
|
+
})
|
|
174
|
+
.from(schema.brainSearchArtifacts)
|
|
175
|
+
.innerJoin(
|
|
176
|
+
schema.brainSources,
|
|
177
|
+
eq(schema.brainSources.id, schema.brainSearchArtifacts.sourceId),
|
|
178
|
+
)
|
|
179
|
+
.innerJoin(
|
|
180
|
+
schema.brainRawCaptures,
|
|
181
|
+
eq(schema.brainRawCaptures.id, schema.brainSearchArtifacts.captureId),
|
|
182
|
+
)
|
|
183
|
+
.where(
|
|
184
|
+
and(
|
|
185
|
+
baseFilter,
|
|
186
|
+
artifactIds?.length
|
|
187
|
+
? inArray(schema.brainSearchArtifacts.id, artifactIds)
|
|
188
|
+
: undefined,
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
const recentRows = await selectArtifacts()
|
|
192
|
+
.orderBy(desc(schema.brainSearchArtifacts.capturedAt))
|
|
193
|
+
.limit(Math.max((input.limit ?? 25) * 5, 50));
|
|
194
|
+
let ftsRanks = new Map<string, number>();
|
|
195
|
+
let semanticRanks = new Map<string, number>();
|
|
196
|
+
if (isPostgres()) {
|
|
197
|
+
try {
|
|
198
|
+
const fts = await queryPostgresFts(getDbExec(), {
|
|
199
|
+
query: input.query,
|
|
200
|
+
allowedAudienceIds: audienceIds,
|
|
201
|
+
limit: Math.max((input.limit ?? 25) * 3, 30),
|
|
202
|
+
namespace: SEARCH_NAMESPACE,
|
|
203
|
+
});
|
|
204
|
+
ftsRanks = new Map(fts.map((hit, index) => [hit.chunkId, index + 1]));
|
|
205
|
+
const family = defaultEmbeddingFamily(await availableEmbeddingFamilies());
|
|
206
|
+
if (family) {
|
|
207
|
+
const [queryVector] = await family.embed(
|
|
208
|
+
[{ text: input.query }],
|
|
209
|
+
"query",
|
|
210
|
+
);
|
|
211
|
+
if (queryVector) {
|
|
212
|
+
const vectorHits = await queryPgVectorIndex(
|
|
213
|
+
getDbExec(),
|
|
214
|
+
{
|
|
215
|
+
embeddingSetId: family.id,
|
|
216
|
+
dimensions: family.dimensions,
|
|
217
|
+
vector: queryVector,
|
|
218
|
+
allowedAudienceIds: audienceIds,
|
|
219
|
+
limit: Math.max((input.limit ?? 25) * 3, 30),
|
|
220
|
+
namespace: SEARCH_NAMESPACE,
|
|
221
|
+
},
|
|
222
|
+
true,
|
|
223
|
+
);
|
|
224
|
+
const vectorKeys = vectorHits.map((hit) => hit.vectorKey);
|
|
225
|
+
const embeddingRows = vectorKeys.length
|
|
226
|
+
? await db
|
|
227
|
+
.select({
|
|
228
|
+
vectorKey: schema.brainSearchEmbeddings.vectorKey,
|
|
229
|
+
targetType: schema.brainSearchEmbeddings.targetType,
|
|
230
|
+
targetId: schema.brainSearchEmbeddings.targetId,
|
|
231
|
+
})
|
|
232
|
+
.from(schema.brainSearchEmbeddings)
|
|
233
|
+
.where(
|
|
234
|
+
and(
|
|
235
|
+
eq(schema.brainSearchEmbeddings.status, "active"),
|
|
236
|
+
inArray(
|
|
237
|
+
schema.brainSearchEmbeddings.audienceId,
|
|
238
|
+
audienceIds,
|
|
239
|
+
),
|
|
240
|
+
inArray(schema.brainSearchEmbeddings.vectorKey, vectorKeys),
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
: [];
|
|
244
|
+
const burstIds = embeddingRows
|
|
245
|
+
.filter((row) => row.targetType === "burst")
|
|
246
|
+
.map((row) => row.targetId);
|
|
247
|
+
const burstArtifacts = burstIds.length
|
|
248
|
+
? await db
|
|
249
|
+
.select({
|
|
250
|
+
id: schema.brainSearchBursts.id,
|
|
251
|
+
artifactId: schema.brainSearchBursts.artifactId,
|
|
252
|
+
})
|
|
253
|
+
.from(schema.brainSearchBursts)
|
|
254
|
+
.where(inArray(schema.brainSearchBursts.id, burstIds))
|
|
255
|
+
: [];
|
|
256
|
+
const burstToArtifact = new Map(
|
|
257
|
+
burstArtifacts.map((row) => [row.id, row.artifactId]),
|
|
258
|
+
);
|
|
259
|
+
const keyToArtifact = new Map(
|
|
260
|
+
embeddingRows.map((row) => [
|
|
261
|
+
row.vectorKey,
|
|
262
|
+
row.targetType === "artifact"
|
|
263
|
+
? row.targetId
|
|
264
|
+
: burstToArtifact.get(row.targetId),
|
|
265
|
+
]),
|
|
266
|
+
);
|
|
267
|
+
semanticRanks = new Map();
|
|
268
|
+
vectorHits.forEach((hit, index) => {
|
|
269
|
+
const artifactId = keyToArtifact.get(hit.vectorKey);
|
|
270
|
+
if (artifactId && !semanticRanks.has(artifactId))
|
|
271
|
+
semanticRanks.set(artifactId, index + 1);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
ftsRanks = new Map();
|
|
277
|
+
semanticRanks = new Map();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const externalIds = Array.from(
|
|
281
|
+
new Set([...ftsRanks.keys(), ...semanticRanks.keys()]),
|
|
282
|
+
);
|
|
283
|
+
const externalRows = externalIds.length
|
|
284
|
+
? await selectArtifacts(externalIds)
|
|
285
|
+
: [];
|
|
286
|
+
const rowMap = new Map(
|
|
287
|
+
[...recentRows, ...externalRows].map((row) => [row.id, row]),
|
|
288
|
+
);
|
|
289
|
+
const rows = [...rowMap.values()];
|
|
290
|
+
const corpus = rows.map(
|
|
291
|
+
(row) => `${row.title} ${row.question} ${row.summary} ${row.resolution}`,
|
|
292
|
+
);
|
|
293
|
+
const lexicalCandidates = rows
|
|
294
|
+
.map((row) => ({
|
|
295
|
+
id: row.id,
|
|
296
|
+
artifactId: row.id,
|
|
297
|
+
captureId: row.captureId,
|
|
298
|
+
sourceId: row.sourceId,
|
|
299
|
+
audienceId: row.audienceId,
|
|
300
|
+
title: row.title,
|
|
301
|
+
text: `${row.question} ${row.summary} ${row.resolution}`,
|
|
302
|
+
capturedAt: row.capturedAt,
|
|
303
|
+
lexicalRank: undefined,
|
|
304
|
+
authority: 1,
|
|
305
|
+
freshness: Math.max(
|
|
306
|
+
0,
|
|
307
|
+
1 -
|
|
308
|
+
(Date.now() - Date.parse(row.capturedAt)) /
|
|
309
|
+
(1000 * 60 * 60 * 24 * 365),
|
|
310
|
+
),
|
|
311
|
+
lane: "lexical" as const,
|
|
312
|
+
}))
|
|
313
|
+
.map((candidate) => ({
|
|
314
|
+
candidate,
|
|
315
|
+
lexical: lexicalScore(
|
|
316
|
+
`${candidate.title} ${candidate.text}`,
|
|
317
|
+
terms,
|
|
318
|
+
corpus,
|
|
319
|
+
),
|
|
320
|
+
}))
|
|
321
|
+
.filter(({ lexical }) => lexical > 0)
|
|
322
|
+
.sort((a, b) => b.lexical - a.lexical)
|
|
323
|
+
.map(({ candidate }, index) => ({ ...candidate, lexicalRank: index + 1 }));
|
|
324
|
+
const lexicalRanks = new Map(
|
|
325
|
+
lexicalCandidates.map((candidate) => [candidate.id, candidate.lexicalRank]),
|
|
326
|
+
);
|
|
327
|
+
const candidates = rows
|
|
328
|
+
.filter(
|
|
329
|
+
(row) =>
|
|
330
|
+
lexicalRanks.has(row.id) ||
|
|
331
|
+
ftsRanks.has(row.id) ||
|
|
332
|
+
semanticRanks.has(row.id),
|
|
333
|
+
)
|
|
334
|
+
.map((row) => {
|
|
335
|
+
const lexicalRank = ftsRanks.get(row.id) ?? lexicalRanks.get(row.id);
|
|
336
|
+
const semanticRank = semanticRanks.get(row.id);
|
|
337
|
+
return {
|
|
338
|
+
id: row.id,
|
|
339
|
+
artifactId: row.id,
|
|
340
|
+
captureId: row.captureId,
|
|
341
|
+
sourceId: row.sourceId,
|
|
342
|
+
audienceId: row.audienceId,
|
|
343
|
+
title: row.title,
|
|
344
|
+
text: `${row.question} ${row.summary} ${row.resolution}`,
|
|
345
|
+
capturedAt: row.capturedAt,
|
|
346
|
+
lexicalRank,
|
|
347
|
+
semanticRank,
|
|
348
|
+
authority: 1,
|
|
349
|
+
freshness: Math.max(
|
|
350
|
+
0,
|
|
351
|
+
1 -
|
|
352
|
+
(Date.now() - Date.parse(row.capturedAt)) /
|
|
353
|
+
(1000 * 60 * 60 * 24 * 365),
|
|
354
|
+
),
|
|
355
|
+
lane: semanticRank
|
|
356
|
+
? lexicalRank
|
|
357
|
+
? ("hybrid" as const)
|
|
358
|
+
: ("semantic" as const)
|
|
359
|
+
: ("lexical" as const),
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
return reciprocalRankFusion(candidates).slice(0, input.limit ?? 25);
|
|
363
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { and, eq, isNull, lt, ne, or } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import { getDb, schema } from "../db/index.js";
|
|
4
|
+
import { nanoid, nowIso, stableJson } from "./brain.js";
|
|
5
|
+
import type {
|
|
6
|
+
BrainCaptureInvalidation,
|
|
7
|
+
BrainIngestOperation,
|
|
8
|
+
} from "./search-index-contracts.js";
|
|
9
|
+
|
|
10
|
+
export async function enqueueBrainOperation(input: {
|
|
11
|
+
operation: BrainIngestOperation;
|
|
12
|
+
dedupeKey: string;
|
|
13
|
+
sourceId?: string | null;
|
|
14
|
+
captureId?: string | null;
|
|
15
|
+
priority?: number;
|
|
16
|
+
runAfter?: string | null;
|
|
17
|
+
payload?: Record<string, unknown>;
|
|
18
|
+
}) {
|
|
19
|
+
const db = getDb();
|
|
20
|
+
const now = nowIso();
|
|
21
|
+
const id = nanoid();
|
|
22
|
+
await db
|
|
23
|
+
.insert(schema.brainIngestQueue)
|
|
24
|
+
.values({
|
|
25
|
+
id,
|
|
26
|
+
sourceId: input.sourceId ?? null,
|
|
27
|
+
captureId: input.captureId ?? null,
|
|
28
|
+
operation: input.operation,
|
|
29
|
+
status: "queued",
|
|
30
|
+
priority: input.priority ?? 50,
|
|
31
|
+
attempts: 0,
|
|
32
|
+
payloadJson: stableJson(input.payload ?? {}),
|
|
33
|
+
dedupeKey: input.dedupeKey,
|
|
34
|
+
leaseToken: null,
|
|
35
|
+
leaseExpiresAt: null,
|
|
36
|
+
error: null,
|
|
37
|
+
runAfter: input.runAfter ?? null,
|
|
38
|
+
createdAt: now,
|
|
39
|
+
updatedAt: now,
|
|
40
|
+
})
|
|
41
|
+
.onConflictDoUpdate({
|
|
42
|
+
target: schema.brainIngestQueue.dedupeKey,
|
|
43
|
+
set: {
|
|
44
|
+
sourceId: input.sourceId ?? null,
|
|
45
|
+
captureId: input.captureId ?? null,
|
|
46
|
+
operation: input.operation,
|
|
47
|
+
status: "queued",
|
|
48
|
+
priority: input.priority ?? 50,
|
|
49
|
+
payloadJson: stableJson(input.payload ?? {}),
|
|
50
|
+
leaseToken: null,
|
|
51
|
+
leaseExpiresAt: null,
|
|
52
|
+
error: null,
|
|
53
|
+
runAfter: input.runAfter ?? null,
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
},
|
|
56
|
+
setWhere: or(
|
|
57
|
+
ne(schema.brainIngestQueue.status, "processing"),
|
|
58
|
+
isNull(schema.brainIngestQueue.leaseExpiresAt),
|
|
59
|
+
lt(schema.brainIngestQueue.leaseExpiresAt, now),
|
|
60
|
+
),
|
|
61
|
+
});
|
|
62
|
+
const [row] = await db
|
|
63
|
+
.select()
|
|
64
|
+
.from(schema.brainIngestQueue)
|
|
65
|
+
.where(eq(schema.brainIngestQueue.dedupeKey, input.dedupeKey))
|
|
66
|
+
.limit(1);
|
|
67
|
+
return row;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function enqueueCaptureInvalidation(
|
|
71
|
+
invalidation: BrainCaptureInvalidation,
|
|
72
|
+
) {
|
|
73
|
+
const version =
|
|
74
|
+
invalidation.next?.contentHash ??
|
|
75
|
+
invalidation.previous?.contentHash ??
|
|
76
|
+
"deleted";
|
|
77
|
+
const operation = invalidation.next ? "search-index" : "search-unindex";
|
|
78
|
+
return enqueueBrainOperation({
|
|
79
|
+
operation,
|
|
80
|
+
dedupeKey: `${operation}:${invalidation.captureId}:${version}`,
|
|
81
|
+
sourceId: invalidation.sourceId,
|
|
82
|
+
captureId: invalidation.captureId,
|
|
83
|
+
priority: operation === "search-unindex" ? 10 : 40,
|
|
84
|
+
payload: { invalidation },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function markBrainOperationDone(id: string, leaseToken: string) {
|
|
89
|
+
return getDb()
|
|
90
|
+
.update(schema.brainIngestQueue)
|
|
91
|
+
.set({
|
|
92
|
+
status: "done",
|
|
93
|
+
leaseToken: null,
|
|
94
|
+
leaseExpiresAt: null,
|
|
95
|
+
updatedAt: nowIso(),
|
|
96
|
+
})
|
|
97
|
+
.where(
|
|
98
|
+
and(
|
|
99
|
+
eq(schema.brainIngestQueue.id, id),
|
|
100
|
+
eq(schema.brainIngestQueue.status, "processing"),
|
|
101
|
+
eq(schema.brainIngestQueue.leaseToken, leaseToken),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BrainSettings } from "../../shared/types.js";
|
|
2
|
+
|
|
3
|
+
export interface BrainPrivacyReadiness {
|
|
4
|
+
configured: boolean;
|
|
5
|
+
model: string | null;
|
|
6
|
+
engine: string | null;
|
|
7
|
+
warning: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function brainPrivacyReadiness(
|
|
11
|
+
settings: BrainSettings,
|
|
12
|
+
): BrainPrivacyReadiness {
|
|
13
|
+
const model = settings.privacyClassifierModel?.trim() || null;
|
|
14
|
+
const engine = settings.privacyClassifierEngine?.trim() || null;
|
|
15
|
+
const configured = Boolean(model && engine);
|
|
16
|
+
return {
|
|
17
|
+
configured,
|
|
18
|
+
model: configured ? model : null,
|
|
19
|
+
engine: configured ? engine : null,
|
|
20
|
+
warning: configured
|
|
21
|
+
? null
|
|
22
|
+
: "No approved privacy classifier is configured. Deterministic-clean captures may be stored; uncertain captures are quarantined and deterministic hard-category captures are suppressed. Neither can be searched, cited, distilled, or exposed to agents.",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export const BRAIN_SEARCH_INDEX_VERSION = "1";
|
|
2
|
+
export const BRAIN_SENSITIVITY_POLICY_VERSION = "1";
|
|
3
|
+
|
|
4
|
+
export type BrainSensitivityCategory =
|
|
5
|
+
| "performance"
|
|
6
|
+
| "discipline"
|
|
7
|
+
| "termination"
|
|
8
|
+
| "layoff-reorg"
|
|
9
|
+
| "compensation"
|
|
10
|
+
| "recruiting"
|
|
11
|
+
| "health-accommodation"
|
|
12
|
+
| "investigation"
|
|
13
|
+
| "privileged-legal"
|
|
14
|
+
| "secret-credential"
|
|
15
|
+
| "personal";
|
|
16
|
+
|
|
17
|
+
export type BrainSensitivityDisposition =
|
|
18
|
+
| "allowed"
|
|
19
|
+
| "suppressed"
|
|
20
|
+
| "quarantined";
|
|
21
|
+
|
|
22
|
+
export interface BrainSafeSegment {
|
|
23
|
+
id: string;
|
|
24
|
+
authorKey?: string;
|
|
25
|
+
capturedAt: string;
|
|
26
|
+
sourceUrl?: string;
|
|
27
|
+
text: string;
|
|
28
|
+
reactionCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BrainSensitivityDecision {
|
|
32
|
+
disposition: BrainSensitivityDisposition;
|
|
33
|
+
categories: BrainSensitivityCategory[];
|
|
34
|
+
confidenceBand: "deterministic" | "high" | "medium" | "uncertain";
|
|
35
|
+
policyVersion: string;
|
|
36
|
+
safeSegments: BrainSafeSegment[];
|
|
37
|
+
safeContent: string;
|
|
38
|
+
classifier: "deterministic" | "approved-model";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface BrainAudienceAssignment {
|
|
42
|
+
audienceId: string;
|
|
43
|
+
aclHash: string;
|
|
44
|
+
kind: "org" | "slack-private-channel" | "meeting" | "restricted";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface BrainSearchStalenessKey {
|
|
48
|
+
contentHash: string;
|
|
49
|
+
indexVersion: string;
|
|
50
|
+
sensitivityPolicyVersion: string;
|
|
51
|
+
aclHash: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type BrainIngestOperation =
|
|
55
|
+
| "distill"
|
|
56
|
+
| "sync"
|
|
57
|
+
| "search-index"
|
|
58
|
+
| "search-unindex"
|
|
59
|
+
| "slack-thread-refresh";
|
|
60
|
+
|
|
61
|
+
export interface BrainCaptureInvalidation {
|
|
62
|
+
captureId: string;
|
|
63
|
+
sourceId: string;
|
|
64
|
+
reason:
|
|
65
|
+
| "content-changed"
|
|
66
|
+
| "sensitivity-changed"
|
|
67
|
+
| "access-changed"
|
|
68
|
+
| "source-deleted"
|
|
69
|
+
| "upstream-deleted";
|
|
70
|
+
previous?: Partial<BrainSearchStalenessKey>;
|
|
71
|
+
next?: Partial<BrainSearchStalenessKey>;
|
|
72
|
+
}
|