@gmickel/gno 1.12.3 → 1.13.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 +57 -30
- package/assets/skill/SKILL.md +6 -1
- package/assets/skill/cli-reference.md +16 -6
- package/assets/skill/mcp-reference.md +22 -3
- package/package.json +2 -1
- package/src/app/constants.ts +43 -10
- package/src/app/index-name.ts +127 -0
- package/src/cli/commands/doctor-activation.ts +151 -0
- package/src/cli/commands/doctor.ts +41 -16
- package/src/cli/commands/get.ts +18 -0
- package/src/cli/commands/mcp/atomic-config-write.ts +118 -0
- package/src/cli/commands/mcp/config-discovery.ts +42 -0
- package/src/cli/commands/mcp/config-editors.ts +432 -0
- package/src/cli/commands/mcp/config.ts +63 -160
- package/src/cli/commands/mcp/install.ts +75 -37
- package/src/cli/commands/mcp/paths.ts +141 -136
- package/src/cli/commands/mcp/server-entry.ts +66 -0
- package/src/cli/commands/mcp/status.ts +189 -57
- package/src/cli/commands/mcp/target-display.ts +30 -0
- package/src/cli/commands/mcp/uninstall.ts +29 -31
- package/src/cli/commands/mcp/yaml-config-editor.ts +257 -0
- package/src/cli/commands/mcp/yaml-layout-scanner.ts +447 -0
- package/src/cli/commands/multi-get.ts +31 -6
- package/src/cli/commands/status.ts +107 -11
- package/src/cli/program.ts +66 -20
- package/src/core/activation-connector-health.ts +19 -0
- package/src/core/activation-probe-plan.ts +321 -0
- package/src/core/activation-probe.ts +138 -0
- package/src/core/activation-receipt-store.ts +39 -0
- package/src/core/activation-status.ts +513 -0
- package/src/core/activation-verifier.ts +416 -0
- package/src/core/connector-environment.ts +68 -0
- package/src/core/connector-policy.ts +233 -0
- package/src/core/connector-verification-target.ts +150 -0
- package/src/core/connector-verifier.ts +497 -0
- package/src/core/context-resolver.ts +285 -0
- package/src/core/indexed-reference.ts +33 -8
- package/src/core/runtime-entrypoint.ts +24 -0
- package/src/mcp/activation-verification-mode.ts +4 -0
- package/src/mcp/server.ts +9 -2
- package/src/mcp/tools/index.ts +3 -3
- package/src/pipeline/answer-prompt.ts +80 -0
- package/src/pipeline/answer.ts +12 -26
- package/src/pipeline/hybrid.ts +2 -0
- package/src/pipeline/result-context.ts +51 -0
- package/src/pipeline/search.ts +5 -1
- package/src/pipeline/vsearch.ts +2 -0
- package/src/sdk/client.ts +7 -0
- package/src/sdk/types.ts +1 -0
- package/src/serve/activation-health.ts +91 -0
- package/src/serve/background-runtime.ts +11 -1
- package/src/serve/connectors.ts +164 -19
- package/src/serve/public/components/BootstrapStatus.tsx +94 -1
- package/src/serve/public/components/FirstRunWizard.tsx +13 -51
- package/src/serve/public/components/HealthCenter.tsx +8 -2
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Connectors.tsx +216 -55
- package/src/serve/public/pages/Dashboard.tsx +1 -0
- package/src/serve/routes/api.ts +152 -8
- package/src/serve/server.ts +44 -9
- package/src/serve/status-model.ts +4 -0
- package/src/serve/status.ts +79 -35
- package/src/store/activation-receipts.ts +390 -0
- package/src/store/index.ts +8 -0
- package/src/store/migrations/012-activation-receipts.ts +38 -0
- package/src/store/migrations/013-fts-sync-marker.ts +39 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +320 -53
- package/src/store/types.ts +124 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ephemeral activation probe planning shared by local and connector proofs.
|
|
3
|
+
*
|
|
4
|
+
* Raw probe terms never leave this in-memory plan and must not be serialized,
|
|
5
|
+
* logged, or copied into activation receipts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FtsTokenizer } from "../config/types";
|
|
9
|
+
import type {
|
|
10
|
+
ActivationIndexDocument,
|
|
11
|
+
ActivationIndexIdentity,
|
|
12
|
+
StorePort,
|
|
13
|
+
StoreResult,
|
|
14
|
+
} from "../store/types";
|
|
15
|
+
|
|
16
|
+
import { err, ok } from "../store/types";
|
|
17
|
+
import {
|
|
18
|
+
extractActivationProbeTerms,
|
|
19
|
+
fingerprintActivationIndex,
|
|
20
|
+
} from "./activation-probe";
|
|
21
|
+
|
|
22
|
+
const MAX_PROBE_DOCUMENTS = 16;
|
|
23
|
+
const MAX_PROBE_DOCUMENT_SCANS = 64;
|
|
24
|
+
const MAX_PROBE_ATTEMPTS = 64;
|
|
25
|
+
const MAX_PROBE_CONTENT_CHARS = 32_768;
|
|
26
|
+
const PROBE_RESULT_LIMIT = 8;
|
|
27
|
+
|
|
28
|
+
interface ProbeDocumentReference {
|
|
29
|
+
document: ActivationIndexDocument;
|
|
30
|
+
mirrorHash: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ProbeDocumentCandidates extends ProbeDocumentReference {
|
|
34
|
+
terms: Array<{ term: string; occurrence: number }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface EphemeralActivationProbeCandidate {
|
|
38
|
+
/** Sensitive corpus-derived term. Never serialize or log. */
|
|
39
|
+
term: string;
|
|
40
|
+
documents: ProbeDocumentReference[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface EphemeralActivationProbePlan {
|
|
44
|
+
collection: string;
|
|
45
|
+
fingerprint: string;
|
|
46
|
+
identity: ActivationIndexIdentity;
|
|
47
|
+
activeDocuments: ActivationIndexDocument[];
|
|
48
|
+
candidates: EphemeralActivationProbeCandidate[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface EphemeralActivationProbeMatch {
|
|
52
|
+
/** Sensitive corpus-derived term. Never serialize or log. */
|
|
53
|
+
term: string;
|
|
54
|
+
probeHash: string;
|
|
55
|
+
resultUri: string;
|
|
56
|
+
resultSourceHash: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ActivationProbePlanRevalidation {
|
|
60
|
+
stable: boolean;
|
|
61
|
+
currentPlan: EphemeralActivationProbePlan;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type ActivationProbeMatchResult =
|
|
65
|
+
| { kind: "matched"; value: EphemeralActivationProbeMatch }
|
|
66
|
+
| { kind: "no_probe_term" }
|
|
67
|
+
| { kind: "retrieval_mismatch"; probeHash?: string };
|
|
68
|
+
|
|
69
|
+
function sha256(value: string): string {
|
|
70
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
71
|
+
hasher.update(value);
|
|
72
|
+
return hasher.digest("hex");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function compareText(left: string, right: string): number {
|
|
76
|
+
if (left === right) {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
return left < right ? -1 : 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function collectProbeCandidates(
|
|
83
|
+
store: StorePort,
|
|
84
|
+
documents: ActivationIndexDocument[],
|
|
85
|
+
ftsTokenizer: FtsTokenizer
|
|
86
|
+
): Promise<StoreResult<EphemeralActivationProbeCandidate[]>> {
|
|
87
|
+
const probeDocuments: ProbeDocumentCandidates[] = [];
|
|
88
|
+
let probeDocumentReads = 0;
|
|
89
|
+
for (const document of documents) {
|
|
90
|
+
if (!document.mirrorHash) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (probeDocumentReads >= MAX_PROBE_DOCUMENT_SCANS) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
probeDocumentReads += 1;
|
|
97
|
+
const content = await store.getContentPrefix(
|
|
98
|
+
document.mirrorHash,
|
|
99
|
+
MAX_PROBE_CONTENT_CHARS
|
|
100
|
+
);
|
|
101
|
+
if (!content.ok) {
|
|
102
|
+
return err(
|
|
103
|
+
content.error.code,
|
|
104
|
+
content.error.message,
|
|
105
|
+
content.error.cause
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (content.value === null) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const terms = extractActivationProbeTerms(content.value, ftsTokenizer);
|
|
112
|
+
if (terms.length === 0) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
probeDocuments.push({
|
|
116
|
+
document,
|
|
117
|
+
mirrorHash: document.mirrorHash,
|
|
118
|
+
terms: terms.map((term, occurrence) => ({ term, occurrence })),
|
|
119
|
+
});
|
|
120
|
+
if (probeDocuments.length >= MAX_PROBE_DOCUMENTS) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const documentsByTerm = new Map<string, ProbeDocumentReference[]>();
|
|
126
|
+
for (const probeDocument of probeDocuments) {
|
|
127
|
+
for (const { term } of probeDocument.terms) {
|
|
128
|
+
const references = documentsByTerm.get(term) ?? [];
|
|
129
|
+
references.push({
|
|
130
|
+
document: probeDocument.document,
|
|
131
|
+
mirrorHash: probeDocument.mirrorHash,
|
|
132
|
+
});
|
|
133
|
+
documentsByTerm.set(term, references);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const probeDocument of probeDocuments) {
|
|
138
|
+
probeDocument.terms.sort((left, right) => {
|
|
139
|
+
const frequencyDifference =
|
|
140
|
+
(documentsByTerm.get(left.term)?.length ?? 0) -
|
|
141
|
+
(documentsByTerm.get(right.term)?.length ?? 0);
|
|
142
|
+
if (frequencyDifference !== 0) {
|
|
143
|
+
return frequencyDifference;
|
|
144
|
+
}
|
|
145
|
+
if (left.occurrence !== right.occurrence) {
|
|
146
|
+
return left.occurrence - right.occurrence;
|
|
147
|
+
}
|
|
148
|
+
return compareText(left.term, right.term);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const candidates: EphemeralActivationProbeCandidate[] = [];
|
|
153
|
+
const seenTerms = new Set<string>();
|
|
154
|
+
const rounds = Math.max(
|
|
155
|
+
0,
|
|
156
|
+
...probeDocuments.map(({ terms }) => terms.length)
|
|
157
|
+
);
|
|
158
|
+
for (let round = 0; round < rounds; round += 1) {
|
|
159
|
+
for (const probeDocument of probeDocuments) {
|
|
160
|
+
const term = probeDocument.terms[round]?.term;
|
|
161
|
+
if (!term || seenTerms.has(term)) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
seenTerms.add(term);
|
|
165
|
+
candidates.push({
|
|
166
|
+
term,
|
|
167
|
+
documents: documentsByTerm.get(term) ?? [],
|
|
168
|
+
});
|
|
169
|
+
if (candidates.length >= MAX_PROBE_ATTEMPTS) {
|
|
170
|
+
return ok(candidates);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return ok(candidates);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function createEphemeralActivationProbePlan(
|
|
178
|
+
store: StorePort,
|
|
179
|
+
collection: string,
|
|
180
|
+
options: { collectCandidates?: boolean } = {}
|
|
181
|
+
): Promise<StoreResult<EphemeralActivationProbePlan>> {
|
|
182
|
+
const snapshotResult = await store.getActivationIndexSnapshot(collection);
|
|
183
|
+
if (!snapshotResult.ok) {
|
|
184
|
+
return err(
|
|
185
|
+
snapshotResult.error.code,
|
|
186
|
+
snapshotResult.error.message,
|
|
187
|
+
snapshotResult.error.cause
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const { documents, identity } = snapshotResult.value;
|
|
191
|
+
const activeDocuments = documents
|
|
192
|
+
.filter((document) => document.active)
|
|
193
|
+
.sort((left, right) => {
|
|
194
|
+
const uriDifference = compareText(left.uri, right.uri);
|
|
195
|
+
return uriDifference === 0 ? left.id - right.id : uriDifference;
|
|
196
|
+
});
|
|
197
|
+
const fingerprint = fingerprintActivationIndex({
|
|
198
|
+
collection,
|
|
199
|
+
indexName: identity.indexName,
|
|
200
|
+
schemaVersion: identity.schemaVersion,
|
|
201
|
+
ftsTokenizer: identity.ftsTokenizer,
|
|
202
|
+
ftsStateHash: identity.ftsStateHash,
|
|
203
|
+
documents: activeDocuments,
|
|
204
|
+
});
|
|
205
|
+
const candidates =
|
|
206
|
+
options.collectCandidates === false
|
|
207
|
+
? ok([])
|
|
208
|
+
: await collectProbeCandidates(
|
|
209
|
+
store,
|
|
210
|
+
activeDocuments,
|
|
211
|
+
identity.ftsTokenizer
|
|
212
|
+
);
|
|
213
|
+
if (!candidates.ok) {
|
|
214
|
+
return candidates;
|
|
215
|
+
}
|
|
216
|
+
return ok({
|
|
217
|
+
collection,
|
|
218
|
+
fingerprint,
|
|
219
|
+
identity,
|
|
220
|
+
activeDocuments,
|
|
221
|
+
candidates: candidates.value,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function populateEphemeralActivationProbePlan(
|
|
226
|
+
store: StorePort,
|
|
227
|
+
plan: EphemeralActivationProbePlan
|
|
228
|
+
): Promise<StoreResult<EphemeralActivationProbePlan>> {
|
|
229
|
+
const candidates = await collectProbeCandidates(
|
|
230
|
+
store,
|
|
231
|
+
plan.activeDocuments,
|
|
232
|
+
plan.identity.ftsTokenizer
|
|
233
|
+
);
|
|
234
|
+
if (!candidates.ok) {
|
|
235
|
+
return candidates;
|
|
236
|
+
}
|
|
237
|
+
return ok({ ...plan, candidates: candidates.value });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Re-read the metadata-only activation snapshot and compare it with a plan
|
|
242
|
+
* created before an asynchronous proof. Callers use this optimistic guard
|
|
243
|
+
* immediately before accepting or persisting proof derived from that plan.
|
|
244
|
+
*/
|
|
245
|
+
export async function revalidateEphemeralActivationProbePlan(
|
|
246
|
+
store: StorePort,
|
|
247
|
+
plan: EphemeralActivationProbePlan
|
|
248
|
+
): Promise<StoreResult<ActivationProbePlanRevalidation>> {
|
|
249
|
+
const current = await createEphemeralActivationProbePlan(
|
|
250
|
+
store,
|
|
251
|
+
plan.collection,
|
|
252
|
+
{ collectCandidates: false }
|
|
253
|
+
);
|
|
254
|
+
if (!current.ok) {
|
|
255
|
+
return current;
|
|
256
|
+
}
|
|
257
|
+
return ok({
|
|
258
|
+
stable: current.value.fingerprint === plan.fingerprint,
|
|
259
|
+
currentPlan: current.value,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function findEphemeralActivationProbeMatch(
|
|
264
|
+
store: StorePort,
|
|
265
|
+
plan: EphemeralActivationProbePlan
|
|
266
|
+
): Promise<StoreResult<ActivationProbeMatchResult>> {
|
|
267
|
+
if (plan.candidates.length === 0) {
|
|
268
|
+
return ok({ kind: "no_probe_term" });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const activeDocumentsByUri = new Map(
|
|
272
|
+
plan.activeDocuments.map((document) => [document.uri, document])
|
|
273
|
+
);
|
|
274
|
+
let finalProbeHash: string | undefined;
|
|
275
|
+
for (const candidate of plan.candidates) {
|
|
276
|
+
const digestKey = candidate.documents
|
|
277
|
+
.map(({ mirrorHash }) => mirrorHash)
|
|
278
|
+
.sort(compareText)
|
|
279
|
+
.join("\0");
|
|
280
|
+
finalProbeHash = sha256(`${digestKey}\0${candidate.term}`);
|
|
281
|
+
const search = await store.searchFts(candidate.term, {
|
|
282
|
+
collection: plan.collection,
|
|
283
|
+
limit: PROBE_RESULT_LIMIT,
|
|
284
|
+
snippet: false,
|
|
285
|
+
});
|
|
286
|
+
if (!search.ok) {
|
|
287
|
+
return err(search.error.code, search.error.message, search.error.cause);
|
|
288
|
+
}
|
|
289
|
+
const matchedResult = search.value.find((result) => {
|
|
290
|
+
const activeDocument = result.uri
|
|
291
|
+
? activeDocumentsByUri.get(result.uri)
|
|
292
|
+
: undefined;
|
|
293
|
+
return (
|
|
294
|
+
activeDocument !== undefined &&
|
|
295
|
+
activeDocument.mirrorHash !== null &&
|
|
296
|
+
result.sourceHash === activeDocument.sourceHash &&
|
|
297
|
+
result.mirrorHash === activeDocument.mirrorHash
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
if (!matchedResult) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const matchedDocument = matchedResult.uri
|
|
304
|
+
? activeDocumentsByUri.get(matchedResult.uri)
|
|
305
|
+
: undefined;
|
|
306
|
+
if (!matchedDocument) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
return ok({
|
|
310
|
+
kind: "matched",
|
|
311
|
+
value: {
|
|
312
|
+
term: candidate.term,
|
|
313
|
+
probeHash: finalProbeHash,
|
|
314
|
+
resultUri: matchedResult.uri ?? matchedDocument.uri,
|
|
315
|
+
resultSourceHash:
|
|
316
|
+
matchedResult.sourceHash ?? matchedDocument.sourceHash,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return ok({ kind: "retrieval_mismatch", probeHash: finalProbeHash });
|
|
321
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { FtsTokenizer } from "../config/types";
|
|
2
|
+
import type { ActivationIndexDocument } from "../store/types";
|
|
3
|
+
|
|
4
|
+
const RECEIPT_SCHEMA_VERSION = "1.0";
|
|
5
|
+
const INDEX_IMPLEMENTATION_ID = "documents_fts-bm25-v1";
|
|
6
|
+
const MAX_CONTENT_CHARS_PER_DOCUMENT = 32_768;
|
|
7
|
+
const MAX_CANDIDATES_PER_DOCUMENT = 32;
|
|
8
|
+
const MAX_TERM_CODEPOINTS = 64;
|
|
9
|
+
const CJK_PREFIX_CODEPOINTS = 4;
|
|
10
|
+
const TOKEN_PATTERN = /[\p{L}\p{N}_]+/gu;
|
|
11
|
+
const LETTER_PATTERN = /\p{L}/u;
|
|
12
|
+
const CJK_PATTERN =
|
|
13
|
+
/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
14
|
+
|
|
15
|
+
const STOPWORDS = new Set([
|
|
16
|
+
"a",
|
|
17
|
+
"an",
|
|
18
|
+
"and",
|
|
19
|
+
"are",
|
|
20
|
+
"as",
|
|
21
|
+
"at",
|
|
22
|
+
"be",
|
|
23
|
+
"by",
|
|
24
|
+
"for",
|
|
25
|
+
"from",
|
|
26
|
+
"in",
|
|
27
|
+
"is",
|
|
28
|
+
"it",
|
|
29
|
+
"of",
|
|
30
|
+
"on",
|
|
31
|
+
"or",
|
|
32
|
+
"that",
|
|
33
|
+
"the",
|
|
34
|
+
"this",
|
|
35
|
+
"to",
|
|
36
|
+
"with",
|
|
37
|
+
"der",
|
|
38
|
+
"die",
|
|
39
|
+
"das",
|
|
40
|
+
"und",
|
|
41
|
+
"ein",
|
|
42
|
+
"eine",
|
|
43
|
+
"le",
|
|
44
|
+
"la",
|
|
45
|
+
"les",
|
|
46
|
+
"et",
|
|
47
|
+
"un",
|
|
48
|
+
"une",
|
|
49
|
+
"il",
|
|
50
|
+
"e",
|
|
51
|
+
"per",
|
|
52
|
+
"con",
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
function sha256(value: string): string {
|
|
56
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
57
|
+
hasher.update(value);
|
|
58
|
+
return hasher.digest("hex");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function compareText(left: string, right: string): number {
|
|
62
|
+
if (left === right) {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
return left < right ? -1 : 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeTerm(raw: string, ftsTokenizer: FtsTokenizer): string | null {
|
|
69
|
+
const normalized = raw.normalize("NFC").toLocaleLowerCase("und");
|
|
70
|
+
const codepoints = Array.from(normalized);
|
|
71
|
+
const minimumCodepoints = ftsTokenizer === "trigram" ? 3 : 2;
|
|
72
|
+
if (
|
|
73
|
+
codepoints.length < minimumCodepoints ||
|
|
74
|
+
!LETTER_PATTERN.test(normalized) ||
|
|
75
|
+
STOPWORDS.has(normalized)
|
|
76
|
+
) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (codepoints.length <= MAX_TERM_CODEPOINTS) {
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
82
|
+
if (CJK_PATTERN.test(normalized)) {
|
|
83
|
+
return codepoints.slice(0, CJK_PREFIX_CODEPOINTS).join("");
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Extract bounded Unicode FTS-compatible candidates in occurrence order. */
|
|
89
|
+
export function extractActivationProbeTerms(
|
|
90
|
+
text: string,
|
|
91
|
+
ftsTokenizer: FtsTokenizer = "unicode61"
|
|
92
|
+
): string[] {
|
|
93
|
+
const terms: string[] = [];
|
|
94
|
+
const seen = new Set<string>();
|
|
95
|
+
const input = text.slice(0, MAX_CONTENT_CHARS_PER_DOCUMENT);
|
|
96
|
+
for (const match of input.matchAll(TOKEN_PATTERN)) {
|
|
97
|
+
const term = normalizeTerm(match[0], ftsTokenizer);
|
|
98
|
+
if (!term || seen.has(term)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
seen.add(term);
|
|
102
|
+
terms.push(term);
|
|
103
|
+
if (terms.length >= MAX_CANDIDATES_PER_DOCUMENT) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return terms;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function fingerprintActivationIndex(input: {
|
|
111
|
+
collection: string;
|
|
112
|
+
indexName: string;
|
|
113
|
+
schemaVersion: number;
|
|
114
|
+
ftsTokenizer: string;
|
|
115
|
+
ftsStateHash: string;
|
|
116
|
+
documents: ActivationIndexDocument[];
|
|
117
|
+
}): string {
|
|
118
|
+
const documents = input.documents
|
|
119
|
+
.filter((document) => document.active)
|
|
120
|
+
.map((document) => ({
|
|
121
|
+
uri: document.uri,
|
|
122
|
+
sourceHash: document.sourceHash,
|
|
123
|
+
mirrorHash: document.mirrorHash,
|
|
124
|
+
}))
|
|
125
|
+
.sort((left, right) => compareText(left.uri, right.uri));
|
|
126
|
+
return sha256(
|
|
127
|
+
JSON.stringify({
|
|
128
|
+
receiptSchemaVersion: RECEIPT_SCHEMA_VERSION,
|
|
129
|
+
indexImplementation: INDEX_IMPLEMENTATION_ID,
|
|
130
|
+
indexName: input.indexName,
|
|
131
|
+
schemaVersion: input.schemaVersion,
|
|
132
|
+
ftsTokenizer: input.ftsTokenizer,
|
|
133
|
+
ftsStateHash: input.ftsStateHash,
|
|
134
|
+
collection: input.collection,
|
|
135
|
+
documents,
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ActivationVerificationReceipt,
|
|
3
|
+
StorePort,
|
|
4
|
+
StoreResult,
|
|
5
|
+
} from "../store/types";
|
|
6
|
+
|
|
7
|
+
import { err, ok } from "../store/types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Persist a receipt only after the collection has been synchronized into the
|
|
11
|
+
* store. Config can name a collection before its first sync creates the parent
|
|
12
|
+
* row required by the receipt table's foreign key.
|
|
13
|
+
*/
|
|
14
|
+
export async function persistActivationReceiptForKnownCollection(
|
|
15
|
+
store: StorePort,
|
|
16
|
+
receipt: ActivationVerificationReceipt
|
|
17
|
+
): Promise<StoreResult<ActivationVerificationReceipt>> {
|
|
18
|
+
const collections = await store.getCollections();
|
|
19
|
+
if (!collections.ok) {
|
|
20
|
+
return err(
|
|
21
|
+
collections.error.code,
|
|
22
|
+
collections.error.message,
|
|
23
|
+
collections.error.cause
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (!collections.value.some(({ name }) => name === receipt.collection)) {
|
|
27
|
+
return ok(receipt);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const persisted = await store.upsertActivationReceipt(receipt);
|
|
31
|
+
if (!persisted.ok) {
|
|
32
|
+
return err(
|
|
33
|
+
persisted.error.code,
|
|
34
|
+
persisted.error.message,
|
|
35
|
+
persisted.error.cause
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return ok(receipt);
|
|
39
|
+
}
|